1. 程式人生 > >Java併發-synchronized, 偏向鎖, 輕量級鎖詳解

Java併發-synchronized, 偏向鎖, 輕量級鎖詳解

synchronized概述

synchronized就是所謂的重量級鎖, 但是自從jdk1.6引入了偏向鎖, 輕量級鎖之後, synchronized就沒有那麼重了。

synchronized用法

  • 對於普通同步方法,鎖是當前例項物件
  • 對於靜態同步方法,鎖是當前類的Class物件
  • 對於同步方法塊,鎖是Synchonized括號裡配置的物件

synchronized實現原理

  • 任何物件都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態
  • 使用monitorenter和monitorexit指令實現
    • monitorenter指令是在編譯後插入到同步程式碼塊的開始位置
    • 執行緒執行到monitorenter指令時,將會嘗試獲取物件所對應的monitor的所有權,即嘗試獲得物件的鎖
    • monitorexit是插入到方法結束處和異常處
    • JVM要保證每個monitorenter必須有對應的monitorexit與之配對
  • synchronized用的鎖是存在Java物件頭裡的

Java物件頭

Java物件頭的長度

長 度 內 容 說 明
32/64bit Mark Word 儲存物件的hashCode或鎖資訊
32/64bit Class Metadata Address 儲存到物件型別資料的指標
32/64bit Array length 陣列的長度(如果當前物件是陣列)

Java物件頭的儲存結構

Java物件頭裡的Mark Word裡預設儲存物件的HashCode、分代年齡和鎖標記位

32位JVM的Mark Word的預設儲存結構 :

鎖狀態 25bit 4bit 1bit是否偏向鎖 2bit鎖標誌位
無鎖狀態 物件的hashCode 物件的分代年齡 0 01

64位JVM的Mark Word的預設儲存結構 :

鎖狀態 25bit 31bit 1bit 4bit 1bit 2bit
cms_free 分代年齡 偏向鎖 鎖標誌位
無鎖 unused hasCode 0 01
偏向鎖 ThreadID(51bit)Epoch(2bit) 1 01

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

鎖狀態
25bit
4bit
1bit
2bit
23bit 2bit 是否偏向鎖 鎖標誌位
輕量級鎖 指向棧中鎖記錄的指標 00
重量級鎖 指向互斥量(重量級鎖)的指標 10
GC標記
11
偏向鎖 執行緒ID Epoch 物件分代年齡 1 01

鎖的升級與對比

鎖狀態

在JDK1.6之後, 鎖存在四種狀態, 級別從低到高依次是 :

  • 無鎖狀態
  • 偏向鎖狀態
  • 輕量級鎖狀態
  • 重量級鎖狀態

這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。

偏向鎖

偏向鎖引入原因:

由於大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖

偏向鎖獲取流程

偏向鎖獲取流程

流程說明 :

  1. 執行緒進入同步程式碼塊時, 先判斷物件頭的Mark Word是否無鎖狀態,是否可偏向(鎖標誌位01, 偏向鎖狀態為0), 是的話CAS設定偏向鎖狀態為1, 表示啟用偏向鎖, 並將偏向鎖指向當前執行緒然後執行步驟6, 否則的話繼續進行下面的判斷
  2. 判斷物件頭的Mark Word中是否儲存著指向當前執行緒的偏向鎖, 如果是表示獲取偏向鎖成功, 則執行步驟6, 否則執行步驟3
  3. 判斷Mark Word中偏向鎖標識是否設定為1(表示當前是偏向鎖), 如果是的話指向步驟4 ,否則執行步驟5
  4. 嘗試使用CAS將物件頭的偏向鎖指向當前執行緒, 成功表示獲取偏向鎖成功, 則執行步驟6, 失敗則表示存在競爭, 偏向鎖要升級為輕量級鎖, 偏向鎖撤銷和升級的流程下面再進行說明
  5. 表示已經不是偏向鎖了, 使用CAS競爭鎖
  6. 執行同步程式碼塊

偏向鎖撤銷

偏向鎖獲取和撤銷流程

執行緒1獲取偏向鎖的流程和上面偏向鎖獲取流程一致, 這裡就省略了, 從執行緒2開始對上述流程做一個說明 :

  1. 執行緒2訪問同步程式碼塊, 發現物件頭Mark Word中偏向鎖標誌為1, 鎖標誌位為01, 表示可偏向, 因為執行緒1已經獲取了偏向鎖, 這個時候物件頭的狀態已經由執行緒1更新為偏向鎖狀態了

  2. 檢查物件頭中偏向鎖是否指向了執行緒2, 發現並不是,這時還是指向執行緒1

  3. 嘗試使用CAS將物件頭的偏向鎖指向當前執行緒, CAS替換Mark Word成功表示獲取偏向鎖成功, 這裡由於物件頭中Mark Word已經指向了執行緒1, 所以替換失敗, 需要撤銷偏向鎖

    這裡關於CAS替換Mark Word這一步, 個人的理解就是, 一個偏向鎖只能由一個執行緒獲得, 如果第二個執行緒來試圖獲取偏向鎖時, 偏向模式就宣告結束。根據所物件目前是否處於被鎖定狀態, 執行撤銷偏向鎖恢復到無鎖狀態,或者將偏向鎖升級為輕量級鎖狀態

  4. 撤銷偏向鎖, 需要等待全域性安全點(safepoint)

  5. 首先暫停擁有偏向鎖的執行緒, 檢查持有偏向鎖的執行緒是否存活 , 如果執行緒存活, 則鎖升級為輕量級鎖, 否則進行偏向鎖撤銷

  6. 偏向鎖撤銷之後, 恢復執行緒1, 執行緒2再去以偏向模式獲取偏向鎖

偏向鎖關閉

  • 偏向鎖是預設開啟的,而且開始時間一般是比應用程式啟動慢幾秒,如果不想有這個延遲,那麼可以使用-XX:BiasedLockingStartUpDelay=0;

  • 如果不想要偏向鎖,那麼可以通過-XX:-UseBiasedLocking = false來設定;

輕量級鎖

輕量級鎖加鎖

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

輕量級鎖解鎖

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

輕量級鎖及膨脹流程圖

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

鎖對比

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

參考
方騰飛<Java併發程式設計的藝術>
周志明<深入理解Java虛擬機器>