1. 程式人生 > >synchronize的實現原理

synchronize的實現原理

鎖的資料結構

同步程式碼塊是使用monitorenter和monitorexit指令實現的,任何java物件都有一個monitor與之關聯,當一個monitor被持有後,物件就處於鎖定狀態。

這裡寫圖片描述

這裡寫圖片描述

在執行期間,Mard Word裡儲存的資料會隨著鎖標誌位的變化而變化。Mark Word可能變化為儲存以下資料結構。

這裡寫圖片描述

自旋鎖

  通常我們稱Sychronized鎖是一種重量級鎖,是因為在互斥狀態下,沒有得到鎖的執行緒會被掛起阻塞,而掛起執行緒和恢復執行緒的操作都需要轉入核心態中完成。同時,虛擬機器開發團隊也注意到,許多應用上的資料鎖只會持續很多的一段時間,如果為了這段時間去掛起和恢復執行緒是不值得的,所以引入了自旋鎖。
  自旋鎖與互斥鎖有點類似,只是自旋鎖不會引起呼叫者睡眠,如果自旋鎖已經被別的執行單元保持,呼叫者就一直迴圈在那裡看是 否該自旋鎖的保持者已經釋放了鎖,”自旋”一詞就是因此而得名。

其作用是為了解決某項資源的互斥使用。因為自旋鎖不會引起呼叫者睡眠,所以自旋鎖的效率遠 高於互斥鎖。雖然它的效率比互斥鎖高,但是它也有些不足之處:
  自旋鎖一直佔用CPU,他在未獲得鎖的情況下,一直執行--自旋,所以佔用著CPU,如果不能在很短的時 間內獲得鎖,這無疑會使CPU效率降低。
  自旋鎖在JDK6以後已經預設開啟,可以通過-XX:+UseSpinning引數來開啟。
  但這顯然並不是最好的一種方法,不掛起執行緒的代價就是該執行緒會一直佔用處理器。如果鎖被佔用的時間很短,自旋等待的效果就會很好,反之,自旋會消耗大量處理器資源。因此,自旋的等待時間必須有一定的限度,如果超過限度還沒有獲得鎖,就要掛起執行緒,這個限度預設是10次,可以使用-XX:PreBlockSpin改變。
  在JDK6以後又引入了自適應自旋鎖
,也就說自旋的時間限度不是一個固定值了,而是由上一次同一個鎖的自旋時間及鎖的擁有者狀態來決定。虛擬機器認為,如果同一個鎖物件自旋剛剛成功獲得鎖,那麼下一次很可能獲得鎖,所以允許這次自旋鎖自旋很長時間、而如果某個鎖很少獲得鎖,那麼以後在獲取鎖的過程中可能忽略到自旋過程。

鎖升級

  在Java 6中為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java中,鎖共有4種狀態,級別從低到高依次為:無狀態鎖,偏向鎖,輕量級鎖和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級。  

輕量級鎖

  “輕量級”是相對於使用作業系統互斥量來實現的傳統鎖而言的。但是,首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用產生的效能消耗。在解釋輕量級鎖的執行過程之前,先明白一點,輕量級鎖所適應的場景是執行緒交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。

輕量級鎖的加鎖過程

  1. 在程式碼進入同步塊的時候,如果同步物件鎖狀態為無鎖狀態(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock
    Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝,官方稱之為 Displaced Mark
    Word。這時候執行緒堆疊與物件頭的狀態如圖一所示。
  2. 拷貝物件頭中的Mark Word複製到鎖記錄中。
  3. 拷貝成功後,虛擬機器將使用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指標,並將Lock record裡的owner指標指向object mark word。如果更新成功,則執行步驟(4),否則執行步驟(5)。
  4. 如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且物件Mark
    Word的鎖標誌位設定為“00”,即表示此物件處於輕量級鎖定狀態,這時候執行緒堆疊與物件頭的狀態如圖二所示。
  5. 如果這個更新操作失敗了,虛擬機器首先會檢查物件的Mark Word是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行。否則說明多個執行緒競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標誌的狀態值變為“10”,Mark Word中儲存的就是指向重量級鎖(互斥量)的指標,後面等待鎖的執行緒也要進入阻塞狀態。而當前執行緒便嘗試使用自旋來獲取鎖,自旋就是為了不讓執行緒阻塞,而採用迴圈去獲取鎖的過程。

圖1— 輕量級鎖CAS操作之前堆疊與物件的狀態
這裡寫圖片描述

圖2— 輕量級鎖CAS操作之後堆疊與物件的狀態
這裡寫圖片描述

輕量級鎖的解鎖過程

  1. 通過CAS操作嘗試把執行緒中複製的Displaced Mark Word物件替換當前的Mark Word。
  2. 如果替換成功,整個同步過程就完成了。
  3. 如果替換失敗,說明有其他執行緒嘗試過獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的執行緒。

這裡寫圖片描述

這裡寫圖片描述

偏向鎖

  引入偏向鎖是為了在無多執行緒競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由於一旦出現多執行緒競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的效能損耗必須小於節省下來的CAS原子指令的效能消耗)。上面說過,輕量級鎖是為了線上程交替執行同步塊時提高效能(通過自旋鎖,使無法獲得鎖的執行緒無需立即進入阻塞狀態,而是在一定時間內迴圈以獲得鎖,減少掛起執行緒和恢復執行緒帶來的消耗),而偏向鎖則是在只有一個執行緒執行同步塊時進一步提高效能(在無多執行緒競爭情況下,獲得鎖的執行緒不釋放鎖,以減少CAS操作)。

偏向鎖獲取過程

  1. 訪問Mark Word中偏向鎖的標識是否設定成1,鎖標誌位是否為01——確認為可偏向狀態。
  2. 如果為可偏向狀態,則測試執行緒ID是否指向當前執行緒,如果是,進入步驟(5),否則進入步驟(3)。
  3. 如果執行緒ID並未指向當前執行緒,則通過CAS操作競爭鎖。如果競爭成功,則將Mark
    Word中執行緒ID設定為當前執行緒ID,然後執行(5);如果競爭失敗,執行(4)。
  4. 如果CAS獲取偏向鎖失敗,則表示有競爭。當到達全域性安全點(safepoint)時獲得偏向鎖的執行緒被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的執行緒繼續往下執行同步程式碼。
  5. 執行同步程式碼。

偏向鎖的釋放

  偏向鎖的撤銷在上述第四步驟中有提到。偏向鎖只有遇到其他執行緒嘗試競爭偏向鎖時,持有偏向鎖的執行緒才會釋放鎖,執行緒不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的執行緒,判斷鎖物件是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位為“01”)或輕量級鎖(標誌位為“00”)的狀態。
  
這裡寫圖片描述

重量級鎖、輕量級鎖和偏向鎖之間轉換

這裡寫圖片描述

三種鎖的優缺點比較

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距 如果執行緒間存在鎖競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個執行緒訪問同步塊場景
輕量級鎖 競爭的執行緒不會阻塞,提高了程式的響應速度 如果始終得不到鎖競爭的執行緒使用自旋會消耗CPU 追求響應時間,鎖佔用時間很短
重量級鎖 執行緒競爭不使用自旋,不會消耗CPU 執行緒阻塞,響應時間緩慢 追求吞吐量,鎖佔用時間較長


重量級鎖不使用自旋,獲取鎖失敗的執行緒直接進入阻塞狀態
輕量級鎖使用自旋