本文總結下Synchronized關鍵字的底層實現原理。
一.synchronized介紹
synchronized是JVM內置鎖,通過內部對象Monitor(監視器鎖)來實現,基於進入與退出monitor對象來實現方法與代碼塊的同步,監視器鎖的實現,最終依賴操作系統的Mutex lock(互斥鎖)來實現。
二.synchronized使用方式
synchronized 主要有3種使用方式。
1.同步類方法
public synchronized void method()
{
// todo
}
鎖的是當前類對象;
2.同步代碼塊
public class TestNotes {
private static Object object;
public String decStock() {
synchronized (object) {
//todo
}
return "下單成功";
}
}
或者
public void run() {
synchronized(this) {
//todo
}
}
鎖的是括號裏面的對象;
3.修飾一個類
lass ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}
三.java對象組成
java對象由3部分組成:
- 對象頭 :比較複雜,synchronized加的鎖,就保存在這裏面的某些數據位。對象頭大小是固定的;
- 實例數據 :就是對象的業務數據;
- 對齊填充位 :64位jvm,默認需要對象大小必須位8byte(字節)的整數倍,所以有時候需要對齊填充位。
Mark Word
其中,鎖的不同狀態,就是存在對象頭中的Mark Word區域中,
下圖是32位系統的Mark Word的區域具體分佈:
四.鎖的升級過程
synchronized鎖有如下4種狀態:
- 無鎖,不鎖住資源,多個線程只有一個能修改資源成功,其他線程會重試;
- 偏向鎖,同一個線程獲取同步資源時,沒有別人競爭時,去掉所有同步操作,相當於沒鎖;
- 輕量級鎖,多個線程搶奪同步資源時,沒有獲得鎖的線程使用CAS自旋等待鎖的釋放;
- 重量級鎖,多個線程搶奪同步資源時,使用操作系統的互斥量進行同步,沒有獲得鎖的線程阻塞等待喚醒;
鎖的升級過程:無鎖-》偏向鎖-》輕量級鎖-》重量級鎖。注意,升級並不一定是一級級升的,有可能跨級別,比如由無鎖狀態,直接升級爲輕量級鎖。
下面,我們以這段代碼爲例,分析以下鎖的具體升級過程:
public class TestNotes {
private static Object object;
//減庫存
public String decStock() {
synchronized (object) {
//todo
}
return "下單成功";
}
}
這段代碼中,synchronized鎖的是object實例對象,鎖的具體狀態,就保存在object對象頭部的markword區域。
1. 無鎖狀態:
步驟說明:
1.synchronized鎖的object對象頭部markword區域,最開始鎖狀態標誌位,默認值就是001,也就是無鎖狀態。
2. 偏向鎖狀態:
某刻,線程1執行到同步代碼塊,虛擬機會使用CAS嘗試修改狀態標誌位,修改爲偏向鎖狀態,並且把線程1的線程ID記錄到markword區域的23bit位,進入偏向鎖狀態,如下圖:
進入偏向鎖狀態後,如果沒有其他線程競爭,線程1後續再次訪問同步代碼塊時,猶如沒有鎖一樣,jvm不會再進行CAS加鎖、解鎖等步驟,直接運行同步塊代碼。直到線程1執行完畢後,jvm會釋放偏向鎖,將markword的標識位恢復初始狀態。
3. 輕量級鎖狀態:
線程1在持有偏向鎖期間,線程2來了,下圖右側部分是線程2執行過程:
線程2訪問同步代碼塊,嘗試獲取鎖;此時jvm會檢查線程1的狀態,因爲線程1還持有鎖,jvm不能撤銷線程1的鎖,此時,jvm就會把鎖升級位輕量級鎖,也就是這個23bit區域存了線程1的地址,指向線程1的線程棧中的某塊區域;同時線程棧的這塊內存也保存了指向markword的引用,相當於兩塊區域互換了內容。
上文中描述的過程,是由無鎖,然後變爲偏向鎖,然後是輕量級鎖;
但是有些場景,鎖會直接由無鎖升級爲輕量級鎖,比如下圖過程:
上圖中,某一時刻,同時有兩個線程執行到同步代碼塊,但實際肯定只能有一個線程先進入,假如是線程1,那麼此時就會直接進輕量級鎖狀態。
此時,線程2就會進行CAS自旋,尋找機會獲取輕量級鎖,如下圖:
4. 重量級鎖狀態:
上圖中,進入輕量級鎖狀態後,線程2還會繼續自旋嘗試獲取鎖;這個時候,synchronized並不會立即進入重量級鎖狀態,而是等到線程2 自旋達到一定次數後,jvm才膨脹爲下圖的重量級鎖。這個自旋次數,jdk7及以後可以通過jvm參數設置。
線程1執行完同步代碼塊,jvm嘗試釋放鎖,修改markword爲初始的無鎖狀態,在釋放鎖的時候,發現已經是重量鎖了,說明有其他線程競爭,並且其他線程肯定已經進入了阻塞狀態,那麼jvm在釋放鎖之後,還會喚醒其他進入阻塞狀態的線程。
五.總結
synchronized在jdk 1.6版本進行了優化,性能有了巨大提升,基本上和java鎖性能沒有什麼差異,所以在生產環境中,synchronized能滿足的場景,儘量使用synchronized,簡單方便。
優化的關鍵,是加入了輕量級鎖,使用CAS,還有自適應機制,避免了向底層操作系統申請互斥量,避免了用戶態和內核態的切換,也就是在一定程度上避免了線程上下文的切換,暫時不進入重量級鎖的狀態。