Synchronized偏向鎖和輕量級鎖的升級

Synchronized偏向鎖和輕量級鎖的升級

一、Synchronized實現原理

1、Synchronized鎖的3中形式

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

  • 對於普通同步方法,鎖是當前實例對象。
  • 對於靜態同步方法,鎖是當前類的 Class 對象。
  • 對於同步方法塊,鎖是 Synchonized 括號裏配置的對象。

2、Synchronized在JVM裏的實現

從 JVM 規範中可以看到 Synchonized 在 JVM 裏的實現原理,JVM 基於進入和退出 Monitor 對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步是使用 monitorenter 和 monitorexit 指令實現的,而方法同步是使用另外一種方式實現的,細節在 JVM 規範裏並沒有詳細說明。但是,方法的同步同樣可以使用這兩個指令來實現。

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

3、Java對象頭

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

表1 Java 對象頭的長度

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

表2 Java 對象頭的存儲結構

在運行期間,Mark Word 裏存儲的數據會隨着鎖標誌位的變化而變化。Mark Word 可能變化爲存儲以下4種數據,如表3所示。

表3 Mark Word 的狀態變化

在64位虛擬機下,Mark Word 是 64bit 大小的,其存儲結構如表2-5所示。

表4 Mark Word 的存儲結構

二、鎖升級

1、鎖的4種狀態

無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態(級別從低到高)。

2、偏向鎖

爲什麼要引入偏向鎖?

HotSpot 的作者經過研究發現,大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,爲了讓線程獲得鎖的代價更低而引入了偏向鎖。

偏向鎖的升級

當線程 1 訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程 ID,因爲偏向鎖不會主動釋放鎖,因此以後線程 1 再次獲取鎖的時候,需要比較當前線程的線程 ID 和對象頭中的線程 ID 是否一致,如果一致(還是線程 1 獲取鎖對象),則無需使用 CAS 來加鎖、解鎖;如果不一致(其他線程,如線程 2 要競爭鎖對象,而偏向鎖不會主動釋放因此還是存儲的線程 1 的線程 ID),那麼需要查看對象頭中記錄的線程 1 是否存活,如果沒有存活,那麼鎖對象被重置爲無鎖狀態,其它線程(線程 2)可以競爭將其設置爲偏向鎖;如果存活,那麼立刻查找該線程(線程 1)的棧幀信息,如果還是需要繼續持有這個鎖對象,那麼暫停當前線程 1,撤銷偏向鎖,升級爲輕量級鎖,如果線程 1 不再使用該鎖對象,那麼將鎖對象狀態設爲無鎖狀態,重新偏向新的線程

3、輕量級鎖

爲什麼要引入輕量級鎖?

輕量級鎖考慮的是競爭鎖對象的線程不多,而且線程持有鎖的時間也不長的情景。因爲阻塞線程需要 CPU 從用戶態轉到內核態,代價較大,如果剛剛阻塞不久這個鎖就被釋放了,那這個代價就有點得不償失了,因此這個時候就乾脆不阻塞這個線程,讓它自旋着等待鎖釋放。

輕量級鎖什麼時候升級爲重量級鎖?

線程 1 獲取輕量級鎖時會先把鎖對象的對象頭 Mark Word 複製一份到線程 1 的棧幀中創建的用於存儲鎖記錄的空間(稱爲 Displaced Mark Word),然後使用 CAS 把對象頭中的內容替換爲線程 1 的鎖記錄地址;

如果在線程 1 複製對象頭的同時(在線程 1 CAS 之前),線程 2 也準備獲取鎖,複製了對象頭到線程 2 的鎖記錄空間中,但是在線程 2 CAS 的時候,發現線程 1 已經把對象頭換了,線程 2 的 CAS 失敗,那麼線程 2 就嘗試使用自旋鎖來等待線程 1 釋放鎖。

但是如果自旋的時間太長也不行,因爲自旋是要消耗 CPU 的,因此自旋的次數是有限制的,比如 10 次或者 100 次,如果自旋次數到了線程 1 還沒有釋放鎖,或者線程 1 還在執行,線程2還在自旋等待,這時又有一個線程 3 過來競爭這個鎖對象,那麼這個時候輕量級鎖就會膨脹爲重量級鎖。重量級鎖把除了擁有鎖的線程都阻塞,防止 CPU 空轉。

注意:爲了避免無用的自旋,輕量級鎖一旦膨脹爲重量級鎖就不會再降級爲輕量級鎖了;偏向鎖升級爲輕量級鎖也不能再降級爲偏向鎖。一句話就是鎖可以升級不可以降級,但是偏向鎖狀態可以被重置爲無鎖狀態。

4、這幾種鎖的優缺點(偏向鎖、輕量級鎖、重量級鎖)

總結

一個對象剛開始實例化的時候,沒有任何線程來訪問它的時候。它是可偏向的,意味着,它現在認爲只可能有一個線程來訪問它,所以當第一個線程來訪問它的時候,它會偏向這個線程,此時,對象持有偏向鎖。偏向第一個線程,這個線程在修改對象頭成爲偏向鎖的時候使用 CAS 操作,並將對象頭中的線程 ID 改成自己的 ID,之後再次訪問這個對象時,只需要對比 ID,不需要再使用 CAS 在進行操作。(偏向鎖只進行一次 CAS 操作)

一旦有第二個線程訪問這個對象,因爲偏向鎖不會主動釋放,所以第二個線程可以看到對象是偏向狀態,這時表明在這個對象上已經存在競爭了,檢查原來持有該對象鎖的線程是否依然存活,如果掛了,則可以將對象變爲無鎖狀態,然後重新偏向新的線程。如果原來的線程依然存活,則馬上執行那個線程的操作棧,檢查該對象的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級爲輕量級鎖,(偏向鎖就是這個時候升級爲輕量級鎖的)。

如果不存在使用了,則可以將對象恢復成無鎖狀態,然後重新偏向。輕量級鎖認爲競爭存在,但是競爭的程度很輕,一般兩個線程對於同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個線程就會釋放鎖。但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹爲重量級鎖,重量級鎖使除了擁有鎖的線程以外的線程都阻塞,防止CPU空轉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章