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種資料:
23bit | 2bit | 是否偏向鎖 | 鎖標誌位 | ||
輕量級鎖 | 指向棧中鎖記錄的指標 | 00 | |||
重量級鎖 | 指向互斥量(重量級鎖)的指標 | 10 | |||
GC標記 | 11 | ||||
偏向鎖 | 執行緒ID | Epoch | 物件分代年齡 | 1 | 01 |
鎖的升級與對比
鎖狀態
在JDK1.6之後, 鎖存在四種狀態, 級別從低到高依次是 :
- 無鎖狀態
- 偏向鎖狀態
- 輕量級鎖狀態
- 重量級鎖狀態
這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
偏向鎖
偏向鎖引入原因:
由於大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖
偏向鎖獲取流程
流程說明 :
- 執行緒進入同步程式碼塊時, 先判斷物件頭的Mark Word是否無鎖狀態,是否可偏向(鎖標誌位01, 偏向鎖狀態為0), 是的話CAS設定偏向鎖狀態為1, 表示啟用偏向鎖, 並將偏向鎖指向當前執行緒然後執行步驟6, 否則的話繼續進行下面的判斷
- 判斷物件頭的Mark Word中是否儲存著指向當前執行緒的偏向鎖, 如果是表示獲取偏向鎖成功, 則執行步驟6, 否則執行步驟3
- 判斷Mark Word中偏向鎖標識是否設定為1(表示當前是偏向鎖), 如果是的話指向步驟4 ,否則執行步驟5
- 嘗試使用CAS將物件頭的偏向鎖指向當前執行緒, 成功表示獲取偏向鎖成功, 則執行步驟6, 失敗則表示存在競爭, 偏向鎖要升級為輕量級鎖, 偏向鎖撤銷和升級的流程下面再進行說明
- 表示已經不是偏向鎖了, 使用CAS競爭鎖
- 執行同步程式碼塊
偏向鎖撤銷
執行緒1獲取偏向鎖的流程和上面偏向鎖獲取流程一致, 這裡就省略了, 從執行緒2開始對上述流程做一個說明 :
-
執行緒2訪問同步程式碼塊, 發現物件頭Mark Word中偏向鎖標誌為1, 鎖標誌位為01, 表示可偏向, 因為執行緒1已經獲取了偏向鎖, 這個時候物件頭的狀態已經由執行緒1更新為偏向鎖狀態了
-
檢查物件頭中偏向鎖是否指向了執行緒2, 發現並不是,這時還是指向執行緒1
-
嘗試使用CAS將物件頭的偏向鎖指向當前執行緒, CAS替換Mark Word成功表示獲取偏向鎖成功, 這裡由於物件頭中Mark Word已經指向了執行緒1, 所以替換失敗, 需要撤銷偏向鎖
這裡關於CAS替換Mark Word這一步, 個人的理解就是, 一個偏向鎖只能由一個執行緒獲得, 如果第二個執行緒來試圖獲取偏向鎖時, 偏向模式就宣告結束。根據所物件目前是否處於被鎖定狀態, 執行撤銷偏向鎖恢復到無鎖狀態,或者將偏向鎖升級為輕量級鎖狀態
-
撤銷偏向鎖, 需要等待全域性安全點(safepoint)
-
首先暫停擁有偏向鎖的執行緒, 檢查持有偏向鎖的執行緒是否存活 , 如果執行緒存活, 則鎖升級為輕量級鎖, 否則進行偏向鎖撤銷
-
偏向鎖撤銷之後, 恢復執行緒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虛擬機器>