1. 程式人生 > >【Java多執行緒 鎖優化】鎖的三種狀態切換

【Java多執行緒 鎖優化】鎖的三種狀態切換

引言

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

同步的基礎

術語定義

這裡寫圖片描述

同步的基礎

Java中的每一個物件都可以作為鎖。

  • 對於同步方法,鎖是當前例項物件
  • 對於靜態同步方法,鎖是當前物件的Class物件
  • 對於同步方法塊,鎖是Synchonized括號裡配置的物件。
    當一個執行緒試圖訪問同步程式碼塊時,它首先必須得到鎖,退出或丟擲異常時必須釋放鎖。那麼鎖存在哪裡呢?鎖裡面會儲存什麼資訊呢?

同步的原理

JVM規範規定JVM基於進入和退出Monitor物件來實現方法同步和程式碼塊同步,但兩者的實現細節不一樣。程式碼塊同步是使用monitorenter和monitorexit指令實現,而方法同步是使用另外一種方式實現的,細節在JVM規範裡並沒有詳細說明,但是方法的同步同樣可以使用這兩個指令來實現。monitorenter指令是在編譯後插入到同步程式碼塊的開始位置,而monitorexit是插入到方法結束處和異常處, JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何物件都有一個 monitor 與之關聯,當且一個monitor 被持有後,它將處於鎖定狀態。執行緒執行到 monitorenter 指令時,將會嘗試獲取物件所對應的 monitor 的所有權,即嘗試獲得物件的鎖。

java物件頭

鎖存在Java物件頭裡。如果物件是陣列型別,則虛擬機器用3個Word(字寬)儲存物件頭,如果物件是非陣列型別,則用2字寬儲存物件頭。在32位虛擬機器中,一字寬等於四位元組,即32bit。

這裡寫圖片描述

Java物件頭裡的Mark Word裡預設儲存物件的HashCode分代年齡鎖標記位。32位JVM的Mark Word的預設儲存結構如下:

這裡寫圖片描述

執行期間Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化。Mark Word可能變化為儲存以下4種資料:

這裡寫圖片描述

鎖的升級

ava SE1.6為了減少獲得鎖和釋放鎖所帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6裡鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級

,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率,下文會詳細分析。

偏向鎖

偏向鎖加鎖

Hotspot的作者經過以往的研究發現大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。當一個執行緒訪問同步塊並獲取鎖時,會使用CAS操作在物件頭Mark Word裡儲存鎖偏向的執行緒ID,**以後該執行緒在進入和退出同步塊時不需要進行任何同步操作和CAS操作。

偏向鎖撤銷

偏向鎖的撤銷偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖

  • 偏向鎖的撤銷,它會首先暫停擁有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否活著

  • 如果執行緒不處於活動狀態,則將物件頭設定成無鎖狀態,然後重新偏向其它執行緒

  • 如果執行緒仍然活動著,檢查該物件的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級為輕量級鎖。如果不存在使用了,則重新變為無鎖狀態,然後重新偏向。

  • 最後喚醒暫停的執行緒。

偏向鎖比輕量鎖更容易被終結,輕量鎖是在有鎖競爭出現時升級為重量鎖,而一般偏向鎖是在有不同執行緒申請鎖時升級為輕量鎖,這也就意味著假如一個物件先被執行緒1加鎖解鎖,再被執行緒2加鎖解鎖,這過程中沒有鎖衝突,也一樣會發生偏向鎖失效,不同的是這回要先退化為無鎖的狀態,再加輕量鎖,如果是執行緒1持有鎖,且2也要爭奪偏向鎖,則直接到輕量級鎖狀態

這裡寫圖片描述

輕量級鎖

輕量級加鎖

輕量級鎖加鎖執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。然後執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標。如果成功,當前執行緒獲得鎖,如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖。

這裡寫圖片描述

這裡寫圖片描述

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

輕量級解鎖

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

這裡寫圖片描述

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

這裡寫圖片描述

全流程總結

成為偏向鎖

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

成為輕量級鎖

一旦有第二個執行緒訪問這個物件,因為偏向鎖不會主動釋放,所以第二個執行緒可以看到物件是偏向狀態,這時表明在這個物件上已經存在競爭了,檢查原來持有該物件鎖的執行緒是否依然存活,如果掛了,則可以將物件變為無鎖狀態,然後重新偏向新的執行緒,如果原來的執行緒依然存活,則馬上執行那個執行緒的操作棧,檢查該物件的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級為輕量級鎖,(偏向鎖就是這個時候升級為輕量級鎖的)。假如一個物件先被執行緒1加鎖解鎖,再被執行緒2加鎖解鎖,這過程中沒有鎖衝突,也一樣會發生偏向鎖失效,不同的是這回要先退化為無鎖的狀態,再加輕量鎖,如果是執行緒1持有鎖,且2也要爭奪偏向鎖,則直接到輕量級鎖狀態

成為重量級鎖

輕量級鎖認為競爭存在,但是競爭的程度很輕,一般兩個執行緒對於同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個執行緒就會釋放鎖。 但是當自旋超過一定的次數,或者一個執行緒在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹為重量級鎖,重量級鎖使除了擁有鎖的執行緒以外的執行緒都阻塞,防止CPU空轉。