鎖機制
1.鎖監視器
在 Java 并發編程中,鎖監視器(Monitor) 是對象內部與鎖關聯的同步機制,用于控制多線程對共享資源的訪問。以下是核心要點:
🔒 監視器的核心組成
-
獨占區(Ownership)
- 一次僅允許一個線程持有監視器(即獲得鎖)
- 通過
synchronized
關鍵字實現
-
入口區(Entry Set)
- 競爭鎖的線程隊列(未獲得鎖的線程在此等待)
-
等待區(Wait Set)
- 調用
wait()
的線程釋放鎖后進入此區域 - 需通過
notify()
/notifyAll()
喚醒
- 調用
?? 關鍵操作
操作 | 作用 | 觸發條件 |
---|---|---|
synchronized | 線程嘗試獲取監視器鎖,成功則進入獨占區,失敗則阻塞在入口區 | 進入同步代碼塊/方法時 |
wait() | 釋放鎖并進入等待區,線程狀態變為 WAITING | 必須在持有鎖時調用 (synchronized 內) |
notify() | 隨機喚醒一個等待區的線程(喚醒后需重新競爭鎖) | 必須在持有鎖時調用 |
notifyAll() | 喚醒所有等待區的線程 | 必須在持有鎖時調用 |
🔄 工作流程示例
public class MonitorDemo {private final Object lock = new Object(); // 鎖監視器關聯到此對象public void doWork() throws InterruptedException {synchronized (lock) { // 1. 線程進入獨占區while (條件不滿足) {lock.wait(); // 2. 釋放鎖并進入等待區}// 3. 執行臨界區代碼lock.notifyAll(); // 4. 喚醒其他等待線程}}
}
?? 重要規則
-
鎖綁定對象
每個 Java 對象(包括 Class 對象)都有一個內置監視器synchronized(obj) { ... } // 使用對象監視器 synchronized(MyClass.class) { ... } // 使用類對象的監視器
-
wait/notify
必須持有鎖
未在synchronized
塊中調用會拋出IllegalMonitorStateException
-
虛假喚醒(Spurious Wakeup)
等待線程可能被隨機喚醒(即使未調用notify
),必須用循環檢查條件:while (!condition) { obj.wait(); }
💡 監視器 vs 顯式鎖(ReentrantLock)
特性 | 內置監視器 | ReentrantLock |
---|---|---|
實現方式 | JVM 內置 | JDK 代碼實現 (java.util.concurrent) |
鎖獲取 | 隱式獲取/釋放 | 顯式調用 lock() /unlock() |
等待條件 | 單一等待隊列 | 支持多個 Condition 隊列 |
公平性 | 非公平鎖 | 可選公平/非公平模式 |
中斷響應 | 不支持 | 支持 lockInterruptibly() |
📌 總結
- 監視器 = 鎖 + 等待隊列 + 線程調度機制
synchronized
實現鎖的互斥訪問wait()/notify()
實現線程間協作- 經典應用場景:生產者-消費者模型、線程間條件等待
?? 注意:優先使用
java.util.concurrent
包中的高級工具(如ReentrantLock
、Condition
),它們提供更靈活的同步控制,能避免許多內置監視器的限制。
2.鎖升級
在 Java 并發編程中,鎖升級(Lock Escalation) 是 JVM 對 synchronized
鎖的優化機制,它會根據鎖競爭情況動態調整鎖狀態,從低開銷狀態逐步升級到高開銷狀態。這是 Java 性能優化的關鍵特性(自 JDK 1.6 引入)。
🔄 鎖升級的四個階段
1. 無鎖狀態(No Lock)
- 特征:對象剛創建時,沒有任何線程訪問
- 開銷:無任何鎖操作成本
- 對象頭標志:
001
2. 偏向鎖(Biased Lock)
- 適用場景:單線程重復訪問同步塊
- 優化原理:
- 在對象頭記錄首個獲得鎖的線程ID
- 同一線程后續進入同步塊時無需 CAS 操作
- 對象頭標志:
101
- 升級觸發:當其他線程嘗試獲取鎖時
3. 輕量級鎖(Lightweight Lock)
- 適用場景:多線程交替執行(無實際競爭)
- 實現機制:
- 在棧幀創建鎖記錄(Lock Record)
- 通過 CAS 將對象頭替換為指向鎖記錄的指針
- 成功:獲得鎖;失敗:自旋嘗試
- 對象頭標志:
00
- 升級觸發:自旋超過閾值(默認10次)或自旋時出現第三個線程競爭
4. 重量級鎖(Heavyweight Lock)
- 適用場景:高并發競爭
- 實現機制:
- 通過操作系統 mutex 互斥量實現
- 未獲鎖線程進入阻塞隊列(涉及內核態切換)
- 對象頭標志:
10
- 特點:開銷最大,但保證公平性
🧪 鎖升級過程示例
public class LockEscalationDemo {private static final Object lock = new Object();private static int counter = 0;public static void main(String[] args) {// 階段1: 偏向鎖 (單線程)synchronized (lock) {counter++;}// 階段2: 輕量級鎖 (多線程交替)new Thread(() -> {for (int i = 0; i < 5; i++) {synchronized (lock) { counter++; }}}).start();// 階段3: 重量級鎖 (高并發競爭)for (int i = 0; i < 10; i++) {new Thread(() -> {synchronized (lock) { counter++; }}).start();}}
}
📊 鎖狀態對比表
特性 | 偏向鎖 | 輕量級鎖 | 重量級鎖 |
---|---|---|---|
適用場景 | 單線程訪問 | 多線程交替執行 | 高并發競爭 |
實現方式 | 記錄線程ID | CAS自旋 | 操作系統mutex |
開銷 | 極低 | 中等 | 高 |
競爭處理 | 升級為輕量級鎖 | 自旋失敗則升級 | 線程阻塞 |
對象頭 | 存儲線程ID+epoch | 指向棧中鎖記錄指針 | 指向監視器對象指針 |
是否阻塞 | 否 | 自旋(非阻塞) | 是(內核阻塞) |
公平性 | 無 | 無 | 可配置 |
?? 鎖升級關鍵技術細節
-
Mark Word 結構變化:
// 32位JVM對象頭示例 | 鎖狀態 | 25bit | 4bit | 1bit(偏向) | 2bit(鎖標志) | |----------|----------------|----------|------------|--------------| | 無鎖 | 哈希碼 | 分代年齡 | 0 | 01 | | 偏向鎖 | 線程ID+epoch | 分代年齡 | 1 | 01 | | 輕量級鎖 | 指向鎖記錄指針 | | | 00 | | 重量級鎖 | 指向監視器指針 | | | 10 |
-
批量重偏向(Bulk Rebias):
- 當一類對象的偏向鎖被撤銷超過閾值(默認20次),JVM 會認為該類不適合偏向鎖
- 后續該類的對象會直接進入輕量級鎖狀態
-
鎖消除(Lock Elision):
-
JIT 編譯器對不可能存在共享競爭的鎖進行消除
// 示例:局部StringBuffer的同步會被消除 public String localMethod() {StringBuffer sb = new StringBuffer(); // 局部變量sb.append("Hello");return sb.toString(); }
-
?? 重要注意事項
-
鎖降級不存在:
- 鎖升級是單向過程(偏向→輕量→重量)
- 一旦升級為重量級鎖,不會降級(即使競爭消失)
-
偏向鎖延遲啟動:
-
JVM 啟動后前 4 秒默認禁用偏向鎖(避免初始化時的無效偏向)
# 關閉偏向鎖(JDK 15+默認) -XX:-UseBiasedLocking
-
-
自旋優化:
- 輕量級鎖的自旋次數由 JVM 自適應調整(Adaptive Spinning)
- 基于前一次鎖獲取的成功率動態變化
💡 最佳實踐建議
-
低競爭場景:
- 保持默認設置(允許鎖升級)
- 避免不必要的同步塊
-
高競爭場景:
- 考慮使用
ReentrantLock
替代synchronized
- 利用
java.util.concurrent
高級并發工具
- 考慮使用
-
性能調優:
# 查看鎖競爭情況 -XX:+PrintSynchronizationStatistics# 禁用偏向鎖(若確認高競爭) -XX:-UseBiasedLocking
鎖升級的本質:JVM 在線程安全和執行效率之間尋找最佳平衡點,開發者應理解其原理但避免過度干預自動優化。
3.ABA問題
ABA 問題詳解
在并發編程中,ABA 問題是使用 CAS(Compare-And-Swap)操作時可能遇到的一種經典問題。它發生在共享變量的值經歷了 A→B→A 的變化序列后,CAS 操作無法檢測到中間狀態變化的情況。
🔍 ABA 問題發生機制
問題本質
- CAS 只檢查值是否匹配,不關心值是否被修改過
- 雖然最終值回到了 A,但中間狀態變化被忽略
- 可能導致數據一致性問題
?? 經典案例:無鎖棧實現中的 ABA
public class Stack {private AtomicReference<Node> top = new AtomicReference<>();public void push(Node node) {Node oldTop;do {oldTop = top.get();node.next = oldTop;} while (!top.compareAndSet(oldTop, node));}public Node pop() {Node oldTop;Node newTop;do {oldTop = top.get();if (oldTop == null) return null;newTop = oldTop.next;} while (!top.compareAndSet(oldTop, newTop));return oldTop;}
}
ABA 問題發生場景:
- 線程1讀取棧頂節點 A
- 線程1被掛起
- 線程2彈出 A,棧頂變為 B
- 線程2彈出 B
- 線程2壓入 A(新節點,地址相同)
- 線程1恢復執行,CAS 成功將 A 替換為 C
- 結果:C.next 指向 B,但 B 已被彈出,造成內存錯誤
🛡? ABA 問題解決方案
1. 版本號機制(推薦)
為每個狀態變化添加版本號戳記:
// Java 內置解決方案
AtomicStampedReference<V> // 帶整數戳記的引用
AtomicMarkableReference<V> // 帶布爾標記的引用
實現原理
使用示例
public class ABASolution {private AtomicStampedReference<Integer> value = new AtomicStampedReference<>(0, 0); // 初始值=0, 版本=0public void update(int expectedValue, int newValue) {int[] stampHolder = new int[1];int oldStamp;int newStamp;do {// 讀取當前值和版本int currentValue = value.get(stampHolder);oldStamp = stampHolder[0];// 驗證值是否被修改過if (currentValue != expectedValue) {break; // 值已被其他線程修改}newStamp = oldStamp + 1; // 更新版本號} while (!value.compareAndSet(expectedValue, newValue, oldStamp, newStamp));}
}
2. 不重復使用內存地址
- 確保被替換的對象不會被重用
- 適用于對象池或資源管理場景
- 實現復雜,不推薦作為通用方案
3. 延遲回收(GC 語言中)
- 依賴垃圾回收機制防止對象復用
- 在非 GC 環境(如 C/C++)中不可靠
📊 ABA 問題與其他并發問題對比
問題類型 | 發生場景 | 檢測難度 | 典型解決方案 |
---|---|---|---|
ABA 問題 | CAS 操作 | 高 | 版本號機制 |
競態條件 | 多線程無序訪問 | 中 | 同步鎖 |
死鎖 | 多鎖相互等待 | 低 | 鎖排序、超時機制 |
活鎖 | 線程持續重試失敗 | 中 | 隨機退避策略 |
ABA 問題本質:CAS 操作只能檢查值的相等性,無法檢測值的歷史變化。版本號機制通過添加狀態元數據,將值檢查擴展為狀態機檢查,從而解決這一問題。