JDK1.6 對 synchronized 的鎖優化
1. 背景
在 JDK 1.6 中對鎖的實現引入了大量的優化。
目的
減少鎖操作的開銷。
2. 鎖優化
在看下面的內容之間,希望大家對 Mark Word 有個大體的理解。Java 中一個物件在堆中的記憶體結構是這樣的:
Mark Word 是這樣的:
2.1 適應性自旋鎖
自旋鎖的思想:
讓一個執行緒在請求一個共享資料的鎖時執行忙迴圈(自旋)一段時間,如果在這段時間內能獲得鎖,就可以避免進入阻塞狀態。
自旋鎖的缺點:
需要進行忙迴圈操作佔用 CPU 時間,它只適用於共享資料的鎖定狀態很短的場景。
若鎖被其他執行緒長時間佔用,會帶來許多效能上的開銷。所以自旋的次數不再固定。由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
如果共享資料的鎖定狀態持續時間較短,切換執行緒不值得(會有上下文切換),可以利用自旋鎖嘗試一定的次數。
2.2 鎖消除
JIT 編譯時,會去除不可能存在競爭的鎖。通過 JIT 的逃逸分析來消除一些沒有在當前同步塊以外被其他執行緒共享的資料的鎖的保護,通過逃逸分析在 TLAB 來分配物件,這樣就不存在共享資料帶來的執行緒安全問題。
2.3 鎖粗化
減少不必要的緊連在一起的 lock,unlock 操作,將多個連續的鎖擴充套件成一個範圍更大的鎖。
2.4 偏向鎖(重入鎖)
為了在無執行緒競爭的情況下避免在鎖獲取過程中執行不必要的 CAS 原子指令,因為 CAS 原子指令雖然相對於重量級鎖來說開銷比較小但還是存在非常可觀的本地延遲(因為 CAS 的底層是利用 LOCK 指令 + cmpxchg 彙編指令來保證原子性的,LOCK 指令會鎖匯流排,其他 CPU 的記憶體操作將會被阻塞,因為 CPU 架構如果是 CMU 的話,控制訊號、資料訊號等是通過共享匯流排傳到記憶體控制器中)。減少同一執行緒獲取鎖的代價,省去了大量有關鎖申請的操作。
核心思想
如果一個執行緒獲得了鎖, 那麼鎖就進入偏向模式,此時 Mark Word 的結構也變為偏向鎖結構,當該執行緒再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程只需要檢查 Mark Word 的鎖標記位為偏向鎖以及當前執行緒 Id 等於 Mark Word 的 ThreadId 即可,這樣就省去了大量有關鎖申請的操作。
2.5 輕量級鎖
這種鎖實現的背後基於這樣一種假設,即在真實的情況下我們程式中的大部分同步程式碼一般都處於無鎖競爭狀態(即單執行緒執行環境),在無鎖競爭的情況下完全可以避免呼叫作業系統層面的重量級互斥鎖(重量級鎖的底層就是這樣實現的),只需要依靠一條 CAS 原子指令就可以完成鎖的獲取及釋放。當存在鎖競爭的情況下,執行 CAS 指令失敗的執行緒將呼叫作業系統互斥鎖進入到阻塞狀態,當鎖被釋放的時候被喚醒。
2.5.1 加鎖的過程
主要分為 3 步:
1、線上程進入同步塊的時候,如果同步物件狀態為無鎖狀態(鎖標誌為 01),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄的空間,用來儲存鎖物件目前的 Mark Word 的拷貝。拷貝成功後,虛擬機器將使用 CAS 操作嘗試將物件的 Mark Word 更新為指向 Lock Record 的指標,並將 Lock Record 裡的 owner 指標指向鎖物件的 Mark Word。如果更新成功,則執行 2,否則執行 3。
2、如果這個更新動作成功了,那麼這個執行緒就擁有了該物件的鎖,並且鎖物件的 Mark Word 中的鎖標誌位設定為 "00",即表示此物件處於輕量級鎖定狀態,這時候虛擬機器執行緒棧與堆中鎖物件的物件頭的狀態如圖所示。
3、如果這個更新操作失敗了,虛擬機器首先會檢查鎖物件的 Mark Word 是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行。否則說明多個執行緒競爭鎖,輕量級鎖就要膨脹為重要量級鎖,鎖標誌的狀態值變為 "10",Mark Word 中儲存的就是指向重量級鎖的指標,後面等待鎖的執行緒也要進入阻塞狀態。而當前執行緒便嘗試使用自旋來獲取鎖。自旋失敗後膨脹為重量級鎖,被阻塞。
2.5.2 解鎖的過程
因為虛擬機器執行緒棧幀中的 Displaced Mark Word 是最初的無鎖狀態時的資料結構,所以用它來替換物件頭中的 Mark Word 就可以釋放鎖。如果鎖已經膨脹為重量級,此時是不可以被替換的,所以替換失敗,喚醒被掛起的執行緒。
3. 心得
鎖膨脹的過程
其實就是物件頭中的 Mark Word 資料結構改變的過程。
4. 三種鎖的對比
4.1 偏向鎖
只需要判斷 Mark Word 中的一些值是否正確就行。
只有一個執行緒訪問同步塊時,使用偏向鎖。
4.2 輕量級鎖
需要執行 CAS 操作自旋來獲取鎖。
如果執行同步塊的時間比較少,那麼多個執行緒之間執行使用輕量級鎖交替執行。
4.3 重量級鎖
會發生上下文切換,CPU 狀態從使用者態轉換為核心態執行作業系統提供的互斥鎖,所以系統開銷比較大,響應時間也比較緩慢。
如果執行同步塊的時間比較長,那麼多個執行緒之間剛開始使用輕量級鎖,後面膨脹為重量級鎖。(因為執行同步塊的時間長,執行緒 CAS 自旋獲得輕量級鎖失敗後就會鎖膨脹)
5. 總結
參考書籍:《深入理解 Java 虛擬機器》
搜尋微信公眾號:Java知其所以然,可免費領取某課、Java 後端面經等資源,還有統一環境(教你怎麼配置一套開發環境)視訊領取