閱讀330 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Java並發——Synchronized優化(輕量級鎖、偏向鎖)

1 重量級鎖

在上一篇博客中我們知道,Synchronized的實現依賴於與某個對象向關聯的monitor(監視器)實現,而monitor是基於底層操作係統的Mutex Lock實現的,而基於Mutex Lock實現的同步必須經曆從用戶態到核心態的轉換,這個開銷特別大,成本非常高。所以頻繁的通過Synchronized實現同步會嚴重影響到程序效率,而這種依賴於Mutex Lock實現的鎖機製也被稱為“重量級鎖”,為了減少重量級鎖帶來的性能開銷,JDK對Synchronized進行了種種優化,尤其是在JDK1.5中引入了輕量級鎖和偏向鎖。

熟悉JVM內存模型的同學都知道,每一個Java實例對象在內存中都包含一部分稱之為對象頭的區域,對象頭記錄了對象的運行時數據,這部分運行時數據包括GC分代信息和鎖狀態。

這裏寫圖片描述

鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說隻能從低到高升級,不會出現鎖的降級)。

2 輕量級鎖

輕量級鎖是相對於重量級鎖而言在獲取鎖和釋放鎖時更加高效,但輕量級鎖並不能代替重量級鎖。輕量級鎖適用的場景是在線程交替獲取某個鎖執行同步代碼塊的場景,如果出現多個進程同時競爭同一個鎖時,輕量級鎖會膨脹成重量級鎖。

2.1 輕量級鎖加鎖過程

  • 當代碼進入同步塊時,如果同步對象鎖為無鎖狀態(偏向鎖標識為“0”,鎖標誌位為“01”),則當前執行線程會在當前棧幀中建立一個鎖記錄(Lock Record)用於複製同步對象的Mark Word, 稱之為Displaced Mark Word
  • 係統通過CAS操作將對象的Mark Word更新指向Lock Word,並將同步對象的Owner Thread指定為當前線程。若果操作成功進入步驟3,失敗進入步驟4
  • 如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位設置為“00”,即表示此對象處於輕量級鎖定狀態
  • 如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標誌的狀態值變為“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後麵等待鎖的線程也要進入阻塞狀態。 而當前線程便嚐試使用自旋來獲取鎖,自旋就是為了不讓線程阻塞,而采用循環去獲取鎖的過程

2.2 輕量級鎖的釋放過程

  • 通過CAS操作將Displaced Mark Word替換對象的Mark Word
  • 如果操作成功,同步完成
  • 如果失敗,則說明已經有其他線程競爭當前對象,此時對象的鎖已經升級為重量級鎖。則當前線程在釋放的同時需要通知其他等待線程

3 偏向鎖

偏向鎖是基於一個經驗為前提的:在多線程環境下,存在很多同步塊隻會被一個線程執行。在這樣的情況下,使用重量級鎖或者輕量鎖都不是最經濟的。因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖隻需要在置換ThreadID的時候依賴一次CAS原子指令。輕量級鎖的機製很簡單,當某個同步對象被某個線程獲取後,同步對象會將其thread owner指向該線程。即使在線程退出同步塊之後,同步對象的thread owner依然不會改變。當該線程下次再次進入該代碼塊時,不用再獲取同步對象使用權而直接執行代碼塊。如果有其他線程競爭該同步對象,則偏向鎖失效,偏向鎖將升級為輕量級鎖或重量級鎖。

3.1 偏向鎖的獲取

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

3.2 偏向鎖的釋放

偏向鎖的撤銷在上述第四步驟中有提到**。**偏向鎖隻有遇到其他線程嚐試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖後恢複到未鎖定(標誌位為“01”)或輕量級鎖(標誌位為“00”)的狀態。

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

這裏寫圖片描述

4 其他優化

4.1 適應性自旋

從輕量級鎖獲取的流程中我們知道,當線程在獲取輕量級鎖的過程中執行CAS操作失敗時,是要通過自旋來獲取重量級鎖的。問題在於,自旋是需要消耗CPU的,如果一直獲取不到鎖的話,那該線程就一直處在自旋狀態,白白浪費CPU資源。解決這個問題最簡單的辦法就是指定自旋的次數,例如讓其循環10次,如果還沒獲取到鎖就進入阻塞狀態。但是JDK采用了更聰明的方式——適應性自旋,簡單來說就是線程如果自旋成功了,則下次自旋的次數會更多,如果自旋失敗了,則自旋的次數就會減少。

4.2 鎖粗化

鎖粗化的概念應該比較好理解,就是將多次連接在一起的加鎖、解鎖操作合並為一次,將多個連續的鎖擴展成一個範圍更大的鎖。

4.3 鎖消除

鎖消除即刪除不必要的加鎖操作。根據代碼逃逸技術,如果判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼可以認為這段代碼是線程安全的,不必要加鎖。

最後更新:2017-07-26 09:04:48

  上一篇:go  Java並發——線程間協作(wait、notify、sleep、yield、join)
  下一篇:go  Java並發——Synchronized及其實現原理