更多Java並發文章:https://www.cnblogs.com/hello-shf/category/1619780.html
一、簡介
相信每一個java程序員對synchronized都不會太陌生,尤其是在大家關心的面試環節,不了解synchronize?不好意思,拜拜了您嘞。synchronized作為java一個重要的同步機制,在遠古時代是被人嗤之以鼻的存在,因為在早期,synchronized屬於重量級鎖,即底層采用的是操作系統提供的Mutex lock實現的,為什么說他是重量級的鎖呢,主要是線程間的切換需要操作系統從用戶態切換到核心態,開銷極其大。所以synchronized被人嗤之以鼻也就理所當然了,當然在java1.5之后呢,synchronized引入了偏向鎖,輕量級鎖,以減少對重量級鎖的依賴(減少對重量級鎖的使用是synchronized優化的終極目標),在此之后synchronized重新煥發心機,迎來了第一個春天。
二、預備知識
1,CAS
在學習synchronized之前,我們需要明白CAS(Compare and Swap)是什么鬼,CAS呢在不同的角度有很多我們常聽到的名詞:樂觀鎖,自旋鎖。其實這個CAS在當前的各種中間件或者語言或者數據庫中具有相當重要的地位。synchronized鎖獲取和撤銷中正式使用的CAS自旋操作。
2,重入鎖
什么叫重入鎖呢?很簡單,從互斥鎖的設計上來說,當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,將會處於阻塞狀態,但當一個線程再次請求自己的對象鎖鎖定的臨界資源時,是可重入的即不需要再去獲取鎖。
三、對象頭 - Mark Word
Mark World
Java中每一個對象都可作為鎖。原因是每個對象的對象頭都存在一個32bit的空間記錄着對象的基礎信息。默認記錄對象的hashCode,分帶年齡(GC的知識),所類型,鎖標志位(誰在拿着這把鎖)。。。
記錄這些信息的區域叫做:Mark Word
線程ID即當前持有鎖的線程信息。
鎖標志位:01(默認),00(輕量級鎖),10(重量級鎖)
Monitor
monitor:監視器
你想想JVM怎么知道哪個對象的Mark Word狀態?答案就是這個monitor,monitor是synchronized實現的另一個基礎,任何一個Java對象都有一個monitor與之關聯,當一個monitor被一個線程持有后,他將處於被鎖定狀態。值得注意的一點monitor只作用於重量級鎖中。
四、synchronize鎖升級過程
synchronized鎖有四個狀態:無鎖,偏向鎖,輕量級鎖,重量級鎖
synchronized鎖升級的方向:無鎖 >> 偏向鎖 >> 輕量級鎖 >> 重量級鎖
性能開銷從左到右依次增加。
鎖只會升級不會降級。
1,偏向鎖
大多數情況下,鎖不存在多線程競爭,總是由同一個線程多次獲得。
偏向鎖的使用旨在於減少對輕量級鎖的依賴,偏向鎖的加鎖和解鎖需要使用CAS自旋。
偏向鎖加鎖過程:如果一個線程進入同步代碼塊(synchronized)獲得了鎖,那么鎖就進入了偏向模式,此時Mark Word的結構也就變為偏向結構,當該線程再次進入同步塊(請求鎖時)將不再需要話費CAS操作來加鎖或者獲取鎖,即獲取鎖的過程只需要檢查Mark Word的鎖標記為是否為偏向鎖以及當前線程ID是否等於Mark Word中的threadID即可,這樣就省去了大量有關鎖申請的操作。如果當前線程從Mark Word獲取的鎖標志位為01(偏向鎖)並且ThreadId=當前線程ID,則加鎖成果。
偏向鎖撤銷過程:
偏向鎖是用來一種競爭才釋放鎖的機制,所以當其他線程嘗試競爭(CAS自旋)偏向鎖時,持有偏向鎖的線程才有可能會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼再執行),他會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活着,如果線程不處於活動狀態,則將對象頭設置成無鎖狀態,該鎖會重新偏向競爭者,即Mark Word中ThreadID重新指向競爭者。如果當前線程依然存活,即競爭者會獲取失敗,則偏向鎖會膨脹為輕量級鎖。
關閉偏向鎖:偏向鎖在 Java 6 和 Java 7 里是默認啟用的,但是它在應用程序啟動幾秒鍾之后才激活,如有必要可以使用 JVM 參數來關閉延遲 -XX:BiasedLockingStartupDelay = 0。如果你確定自己應用程序里所有的鎖通常情況下處於競爭狀態,可以通過 JVM 參數關閉偏向鎖 -XX:-UseBiasedLocking=false,那么默認會進入輕量級鎖狀態。
當前這種偏向模式不適合鎖競爭比較激烈的多線程場合。
2,輕量級鎖
輕量級鎖加鎖過程:前面說到偏向鎖由輕量級鎖升級而來,偏向鎖運行在一個線程進入同步塊的情況下,當第二個線程加入鎖掙用的時候,嘗試通過CAS自旋修改Mark Word中的ThreadID,如果替換失敗,如果在一定次數內(自適應自旋機制)還是失敗,偏向鎖就會升級為輕量級鎖,當前如前面所說,偏向鎖要經歷偏向鎖撤銷 -- 到達安全點 -- 膨脹為輕量級鎖。安全點是重點。
輕量級鎖膨脹過程:當持有輕量級鎖的線程正在執行同步代碼塊(持有鎖),此時又有線程來競爭鎖,首先該線程依然會通過CAS自旋替換Mark Word中的ThreadID為本線程的ID,在一定次數內修改失敗(當前鎖被其他線程持有),輕量級鎖會膨脹為重量級鎖。成功則繼續執行知道當前線程執行完成,釋放輕量級鎖。
輕量級鎖比較適合線程交替執行的場景。
3,重量級鎖
輕量級鎖因為競爭激烈,會膨脹為重量級鎖,一旦鎖膨脹為重量級鎖,線程切換將不是通過CAS自旋競爭來切換線程,而是未持有鎖的競爭者將進入阻塞態。線程的狀態切換都是操作系統底層的mutex lock來實現,而這個操作將意味着實現線程之間的切換需要從用戶態轉為核心態,這個成本是非常高的。
詳細的鎖升級過程如下圖所示:
模擬一下以上過程,假設有兩個線程,線程A和線程B
1,當線程A首先進入同步代碼塊
1)檢查鎖狀態:判斷鎖標志位是否為01,如果是即偏向鎖狀態
2)檢查偏向狀態:Mark Word中的ThreadID是否為當前線程
是:當前線程即線程A進入偏向鎖,執行同步代碼塊。
否:進入偏向鎖競爭
2,模擬偏向鎖競爭
假設線程A當前持有偏向鎖,此時,線程B進入同步代碼塊
1)線程B同樣經過1中的1)-- 2)但是Mark Word中的ThreadID == 線程A的ThreadID,即線程B獲取失敗
3)CAS自旋:線程B進入CAS自旋,嘗試去替換ThreadID(CAS自旋采用的是自適應自旋)
成功:獲取到偏向鎖,執行同步代碼塊。
失敗:在一定次數內還是失敗,偏向鎖膨脹為輕量級鎖
3,偏向鎖升級為輕量級鎖
接着以上過程
1)線程B自旋替換ThreadID失敗,當前持有偏向鎖的線程A開始執行偏向鎖撤銷(等待競爭才釋放的機制)
2)線程A到達安全點 ,虛擬機暫停原持有偏向鎖的線程即線程A
3)虛擬機檢查Mark Word中ThreadID指向的線程(線程A)狀態
不活動狀態:已退出同步代碼塊,表示線程A已退出競爭,線程B獲取到偏向鎖
活動狀態:未退出同步代碼塊,鎖膨脹為輕量級鎖。
4,輕量級鎖競爭及膨脹過程
接着以上過程線程A膨脹為輕量級鎖
1)拷貝Mark Word到線程A的線程棧中,修改鎖標志位為00,修改ThreadID指向當前線程即線程A。線程A被喚醒,從安全點繼續執行。
2)線程B開始進入同步代碼塊,線程B發現鎖標志位為00,拷貝對象頭中的Mark Word到自己的線程棧。
3)線程B自旋修改Mark Word中的ThreadID
成功:執行同步代碼塊
失敗:輕量級鎖膨脹為重量級鎖,標志位被修改為 10,指針指向monitor。
5,重量級鎖競爭
synchronized膨脹為重量級鎖之后,線程調度將依賴於操作系統底層的monitor
競爭不到鎖的線程將進入阻塞狀態,線程切換將會導致操作系統內核由用戶態到核心態的轉變(關於這個知識可以參考操作系統進程和線程調度的知識)。
五、synchronized優化
關於synchronized的使用,度娘一下一大把,在此就不在贅述。
1,鎖粒度優化 —— 應用層優化
synchronized作用域:
修飾靜態方法:鎖是當前對象的 Class 對象,即類鎖。
修飾非靜態方法:鎖是當前實例對象,即對象鎖。
修飾代碼塊:鎖是 Synchonized 括號里配置的對象(不要用Test.class這樣等同於類鎖)。
從上至下,鎖粒度是遞減的,其實最推薦使用的還是修飾同步代碼塊,這樣盡量減少線程持有鎖的時間。如果你用的是類鎖,一旦鎖膨脹為重量級鎖,而類本身生命周期可以簡單地理解為=進程,鎖又不會被及時的GC掉,1.6之后對synchronize所做的偏向鎖,輕量級鎖優化等於沒做。
鎖粗化:
原則上我們需要將鎖的粒度盡量的減小,以減少鎖持有的時間。任何事情過度的追求等於浪費,如果對一個對象反復的加鎖解鎖,也是很浪費時間的,所以當出現這種場景,盡量的需要合並同步代碼塊,減少頻繁加鎖和解鎖的資源浪費。
2,自適應自旋鎖 —— 實現層優化
常規的自旋我們一般會這么寫
while(true){...}
無限制的自旋是對CPU資源的極度浪費,JVM為了節省資源的浪費即更加的智能化,采用了自旋自適應鎖,即自旋的次數不再是無限制或者固定次數,將由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來確定。
3,鎖消除 —— JVM編譯層優化
鎖消除即刪除不必要的加鎖操作。JIT編譯期,根據代碼逃逸技術,如果判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那么可以認為這段代碼是線程安全的,不必加鎖。
比如如下代碼:
1 public void add(String str1,String str2){ 2 StringBuffer sb = new StringBuffer(); 3 sb.append(str1).append(str2); 4 }
JVM會傻到用stringBuffer嗎?不會的,在編譯器就給你把stringBuffer方法上的synchronized給優化掉了。
如有錯誤的地方還請留言指正。
原創不易,轉載請注明原文地址:https://www.cnblogs.com/hello-shf/p/12091591.html
參考文獻:
https://www.infoq.cn/article/java-se-16-synchronized/
https://www.cnblogs.com/paddix/p/5405678.html
https://blog.csdn.net/baidu_38083619/article/details/82527461