《Java併發進階系列》之synchronized的實現原理與應用

生活不能等待別人來安排,要自已去爭取和奮鬥;而不論其結果是喜是悲,但可以慰藉的是,你總不枉在這世界上活了一場。

——路遙 《平凡的世界》 《Java併發進階系列》之synchronized的實現原理與應用

關注一下,更多精彩等著您

在多線程併發編程中synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖。但是隨著Java SE 1.6對synchronized進行了各種優化後,有些情況下它就並不那麼重了。本文詳細介紹Java SE 1.6中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖,以及鎖的存儲結構和升級過程。

利用synchronized實現同步的基礎:Java中的每一個對象都可以作為鎖。具體表現為以下3種形式。

1)對於普通同步方法,鎖是當前實例對象。

2)對於靜態同步方法,鎖是當前類的Class對象。

3)對於同步方法塊,鎖是Synchronized括號裡配置的對象。

當一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖。從JVM規範中可以看到synchronized在JVM裡的實現原理,JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是使用另外一種方式實現的,細節在JVM規範裡並沒有詳細說明。但是,方法的同步同樣可以使用這兩個指令來實現。

monitorenter指令是在變異後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。

Java對象頭

synchronized用的鎖是存在Java對象頭裡的。如果對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,如果對象是非對象類型,則用2個字寬存儲對象頭。在32位虛擬機中,1字寬等於4字節,即32bit,Java對象頭的長度,如下所示:

《Java併發進階系列》之synchronized的實現原理與應用

Java對象頭的長度

Java對象頭裡的Mark Word裡默認存儲對象的HashCode、分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構如下所示:

《Java併發進階系列》之synchronized的實現原理與應用

Java對象頭的存儲結構

在運行期間,Mark Word裡存儲的數據會隨著鎖標誌位的變化而變化。Mark Word可能變化為存儲以下4中數據,如下所示:

《Java併發進階系列》之synchronized的實現原理與應用

Mark Word的狀態變化

在64位虛擬機下,Mark Word是64bit大小的,其存儲結構如下所示:

《Java併發進階系列》之synchronized的實現原理與應用

Mark Word的存儲結構

鎖的升級與對比

Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一種有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

偏向鎖

大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低2⃣️引入了偏向鎖。當一個線程訪問同步代碼塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裡存儲鎖偏向的線程ID,以後該線程在進入和退出同步代碼塊時不需要進行CAS操作來加鎖和解鎖,只需要簡單地測試一下對象頭的Mark Word裡是否存儲著指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

1)偏向鎖的撤銷

偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(這個時間點上沒有正在執行的字節碼)。它會首先暫停擁有偏向鎖的線程,然後檢查持有偏向鎖的線程是否活著,如果線程不處於活動狀態,則將對象頭設置成無鎖狀態;如果線程仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要麼重新偏向於其他線程,要麼恢復到無鎖或者標記對象不適合作為偏向鎖,最後喚醒暫停到線程。下圖線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程:


《Java併發進階系列》之synchronized的實現原理與應用

偏向鎖撤銷流程

2)關閉偏向鎖

偏向鎖在Java 6和Java 7裡是默認啟用的,但是它在應用程序啟動幾秒鐘之後才激活,如有必要可以使用JVM參數來關閉延遲 -XX:BiasedLockingStartupDelay=0。如果你確定應用程序裡所有的鎖通常情況下處於競爭狀態,可以通過JVM參數關閉偏向鎖: -XX:-UseBiasedLocking=false,那麼程序默認會進入輕量級鎖狀態。

輕量級鎖

1)輕量級鎖加鎖

線程在執行同步代碼塊之前,JVM會先在當前線程的棧幀中創建用於存儲記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。然後線程嘗試使用CAS將對象頭中的Mark Word替換為之鄉鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

2)輕量級鎖解鎖

輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖是兩個線程同時競爭鎖,導致鎖膨脹的流程圖:

《Java併發進階系列》之synchronized的實現原理與應用

鎖膨脹流程圖

因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。

鎖的優缺點對比

《Java併發進階系列》之synchronized的實現原理與應用

鎖的優缺點對比

歷史文章推薦:

《Java併發進階系列》之volatile的實現原理與應用


分享到:


相關文章: