JDK1.6 對 synchronized 的鎖優化

1. 背景

在 JDK 1.6 中對鎖的實現引入了大量的優化。

目的

減少鎖操作的開銷

2. 鎖優化

在看下面的內容之間,希望大家對 Mark Word 有個大體的理解。Java 中一個對象在堆中的內存結構是這樣的:

img

Mark Word 是這樣的:

img

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。

img

2、如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且鎖對象的 Mark Word 中的鎖標誌位設置爲 "00",即表示此對象處於輕量級鎖定狀態,這時候虛擬機線程棧與堆中鎖對象的對象頭的狀態如圖所示。

img

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. 總結

img

參考書籍:《深入理解 Java 虛擬機》

搜索微信公衆號:Java知其所以然,可免費領取某課、Java 後端面經等資源,還有統一環境(教你怎麼配置一套開發環境)視頻領取。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章