??Java 虛擬機(JVM)中的鎖升級機制(也稱為鎖膨脹)是 HotSpot 虛擬機為了優化 synchronized
關鍵字的性能而引入的一項重要技術。它的核心思想是:根據實際遇到的競爭激烈程度,動態地將鎖從開銷最小的狀態逐步升級到開銷更大的狀態,從而在無競爭或低競爭時減少鎖操作的開銷,而在高競爭時保證必要的互斥性和線程調度能力。
鎖的狀態主要有四種,升級路徑如下:
無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖
鎖只能升級(膨脹),不能降級(雖然理論上重量級鎖在競爭消失后可以降級,但HotSpot 實現中為了簡化,很少進行降級,尤其是從重量級鎖降級)。
1. 無鎖狀態
- 初始狀態: 當一個對象剛被創建出來,且沒有任何線程嘗試獲取它的鎖時,它就處于無鎖狀態。
- 特點: 沒有鎖的開銷。
- 適用場景: 對象從未被同步訪問或只被單個線程訪問(無需同步)。
2. 偏向鎖
- 設計目標: 優化同一個線程重復進入同步塊的場景(無實際競爭)。消除在無競爭情況下的同步原語開銷(如 CAS)。
- 工作原理:
- 當第一個線程(T1)訪問同步塊時,JVM 會檢查對象頭中的 Mark Word。
- 如果當前是無鎖狀態,JVM 使用 CAS 操作嘗試將 Mark Word 中的線程 ID 設置為 T1 的 ID,并將鎖標志位設置為偏向模式。
- 如果 CAS 成功,T1 就持有了該對象的偏向鎖。后續只要 T1 進入這個同步塊,無需再進行任何同步操作(如 CAS 或鎖申請),只需簡單檢查對象頭中的線程 ID 是否還是自己。
- 升級觸發:
- 競爭出現: 當另一個線程(T2)嘗試獲取這個已經被偏向于 T1 的鎖時,偏向鎖就會失效。
- JVM 會撤銷偏向鎖。撤銷過程需要等待持有偏向鎖的線程(T1)到達全局安全點(Safepoint),暫停 T1。
- 檢查 T1 的狀態:
- 如果 T1 已經退出同步塊(不再持有鎖),則將對象頭設置為無鎖狀態(或者根據情況嘗試重新偏向給 T2)。
- 如果 T1 仍在同步塊中,則將鎖升級為輕量級鎖。JVM 會在 T1 的棧幀中創建一個鎖記錄(Lock Record),并將對象頭的 Mark Word 復制到該鎖記錄中(稱為 Displaced Mark Word),然后用 CAS 操作將對象頭指向 T1 棧幀中的鎖記錄地址(輕量級鎖狀態)。
- 特點: 適用于只有一個線程反復訪問同步塊的場景。加鎖解鎖幾乎無額外開銷。撤銷偏向鎖有代價(需要暫停線程)。
- 關閉偏向鎖: 由于偏向鎖在存在競爭時撤銷有開銷,且現代應用中共享數據競爭往往更常見,從 JDK 15 開始,偏向鎖默認被禁用(可通過
-XX:+UseBiasedLocking
開啟,但已不推薦)。
3. 輕量級鎖
- 設計目標: 優化多個線程交替執行同步塊,但未發生真正并發競爭的場景(低競爭)。避免直接使用重量級鎖帶來的操作系統內核態切換的開銷。
- 工作原理:
- 當線程嘗試獲取輕量級鎖時(可能是從無鎖升級而來,也可能是從偏向鎖撤銷升級而來),JVM 會在當前線程的棧幀中創建一個鎖記錄空間。
- 將對象頭的 Mark Word 復制到該鎖記錄中(稱為 Displaced Mark Word)。
- 然后線程嘗試使用 CAS 操作將對象頭中的 Mark Word 替換為指向該鎖記錄的指針。
- 如果 CAS 成功,當前線程獲得輕量級鎖。鎖標志位變為
00
。 - 如果 CAS 失敗(說明對象頭已被其他線程修改,即發生了競爭),當前線程會自旋(循環嘗試 CAS)一小段時間(自適應自旋)。
- 如果在自旋期間成功獲取到鎖,則繼續執行。
- 如果自旋結束仍未成功,或者自旋過程中競爭加劇(如又有新線程加入競爭),則鎖升級為重量級鎖。
- 如果 CAS 成功,當前線程獲得輕量級鎖。鎖標志位變為
- 解鎖過程: 使用 CAS 操作將 Displaced Mark Word 替換回對象頭。
- 如果成功,解鎖完成。
- 如果失敗(說明鎖已經膨脹為重量級鎖),則在釋放鎖的同時喚醒等待線程。
- 特點: 使用 CAS 和自旋代替互斥量,避免了用戶態到內核態的切換,適用于線程阻塞時間非常短的場景(“忙等”)。自旋會消耗 CPU。如果鎖持有時間較長或競爭激烈,自旋會浪費 CPU,性能反而下降。
- 升級觸發: CAS 失敗且自旋獲取鎖失敗(或自適應策略判斷競爭激烈)、調用
wait()
方法(因為wait()
需要重量級鎖的監視器模型支持)。
4. 重量級鎖
- 設計目標: 處理高并發、激烈競爭的場景。保證在任意時刻只有一個線程能進入同步塊。
- 工作原理:
- 當鎖升級到重量級鎖時,對象頭中的 Mark Word 會指向一個與對象關聯的監視器鎖(Monitor,也稱為管程或互斥鎖),這個結構通常存在于堆中。
- 該 Monitor 內部維護了一個入口隊列(Entry Set) 和一個等待隊列(Wait Set)。
- 當一個線程嘗試獲取重量級鎖時:
- 如果鎖可用(未被持有),則獲取成功,成為鎖的持有者。
- 如果鎖已被其他線程持有,則當前線程會被阻塞(Park),并被操作系統掛起,放入入口隊列等待喚醒。這涉及到用戶態到內核態的切換(線程上下文切換),開銷最大。
- 當持有鎖的線程釋放鎖時,它會喚醒入口隊列中的某個或所有等待線程(具體策略取決于實現,如公平/非公平),被喚醒的線程會重新嘗試獲取鎖。
- 特點: 真正的互斥鎖。阻塞線程,不消耗 CPU 空轉。適用于競爭激烈或臨界區執行時間較長的場景。線程阻塞、喚醒、上下文切換開銷很大。
- 升級觸發: 輕量級鎖自旋失敗、調用
Object.wait()
方法(強制升級)。
總結鎖升級機制
鎖狀態 | 目標場景 | 核心機制 | 優點 | 缺點 | 升級觸發條件 |
---|---|---|---|---|---|
無鎖 | 無同步訪問 | - | 無開銷 | 無法提供線程安全 | 首次線程訪問 |
偏向鎖 | 單線程重復訪問 | CAS 設置 Thread ID | 同一線程后續進入無開銷 | 競爭時撤銷開銷大(需暫停線程) | 第二個線程嘗試獲取鎖 |
輕量級鎖 | 低競爭(交替執行) | CAS + 自旋 (棧鎖記錄) | 避免內核切換,開銷小 | 自旋消耗 CPU,長時間競爭性能下降 | CAS失敗且自旋失敗 / 調用 wait() |
重量級鎖 | 高競爭 | 操作系統 Monitor | 阻塞線程,不消耗 CPU 空轉 | 阻塞/喚醒開銷大(內核切換) | 輕量級鎖升級失敗 / 顯式調用 wait() |
關鍵點
- 自適應自旋: 輕量級鎖中的自旋次數不是固定的,JVM 會根據之前在該鎖上的自旋成功情況以及持有者的狀態,動態調整自旋時間(適應性自旋)。
- 鎖消除: JIT 編譯器在運行時,通過逃逸分析如果發現某個鎖對象不可能被其他線程訪問到(即不會發生競爭),它會將這個鎖操作完全消除掉。
- 鎖粗化: 如果 JVM 檢測到有一連串連續的操作都對同一個對象反復加鎖和解鎖(即使是在循環中),它可能會將加鎖的范圍擴大(粗化)到整個操作序列的外部,從而減少不必要的鎖申請/釋放次數。
- 偏向鎖的爭議與默認關閉: 在現代多核、高并發環境下,共享數據的競爭是常態,偏向鎖在首次獲得和撤銷時的開銷,以及它對應用程序啟動性能的影響(大量類初始化時偏向鎖操作)變得不可忽視。自 JDK 15 起,偏向鎖默認被禁用,輕量級鎖成為更常見的起點。
-XX:-UseBiasedLocking
可以顯式關閉它(在 JDK 15+ 已經是默認行為)。 hashCode()
的影響: 當調用一個未被覆蓋的Object.hashCode()
或System.identityHashCode()
時,如果對象處于偏向鎖或輕量級鎖狀態,會導致鎖撤銷或升級,因為無鎖狀態下的 Mark Word 需要存儲哈希碼。
理解鎖升級的意義
鎖升級機制體現了 JVM 在性能與正確性之間所做的精妙平衡:
- 無競爭/低競爭: 最大程度減少開銷(偏向鎖、輕量級鎖)。
- 高競爭: 保證正確性和線程調度的公平性/效率,接受較大的開銷(重量級鎖)。
了解鎖升級機制對于編寫高效、正確的并發程序至關重要。它解釋了為什么簡單的 synchronized
在低競爭場景下性能可以非常好,而在高競爭場景下性能會顯著下降。在需要極高并發性能的場景下,開發者可能會選擇更靈活、可優化的顯式鎖(如 ReentrantLock
),但 synchronized
結合鎖升級在大多數場景下已經是非常高效且簡潔的選擇。