與自旋鎖「佔用 CPU 等待」不同,阻塞鎖 將無法獲取鎖的線程狀態從 RUNNABLE 轉為 BLOCKED/WAITING,釋放 CPU 資源,由作業系統排程器在鎖釋放時喚醒。本文解析阻塞鎖核心機制、Java 線程狀態轉換、LockSupport 核心 API,並展示基於 CLH 佇列的阻塞鎖實作。
| 狀態 | 說明 | 觸發方式 |
|---|---|---|
| NEW | 建立未啟動 | new Thread() |
| RUNNABLE | 運行中/就緒 | start()、鎖獲取成功 |
| BLOCKED | 等待監視器鎖 | synchronized 進入區 |
| WAITING | 無限期等待 | Object.wait()、LockSupport.park() |
| TIMED_WAITING | 限時等待 | sleep、join、parkNanos、wait(timeout) |
| TERMINATED | 執行結束 | run() 結束、異常終止 |
阻塞鎖流程:嘗試獲取鎖失敗 → 封裝為節點入佇列 → LockSupport.park() 掛起線程 (WAITING) → 釋放鎖時 LockSupport.unpark(thread) 喚醒 → 重新競爭 CPU (RUNNABLE) → 獲取鎖成功
public class LockSupport {
// 掛起當前線程,除非:unpark、中斷、虛假喚醒
public static void park() { ... }
// 帶阻塞物件 (便於除錯監控)
public static void park(Object blocker) { ... }
// 限時掛起 (奈秒精度)
public static void parkNanos(long nanos) { ... }
public static void parkUntil(long deadline) { ... }
// 喚醒指定線程 (許可證機制:最多積累 1 個許可)
public static void unpark(Thread thread) { ... }
// 取得阻塞物件 (JDK 8+)
public static Object getBlocker(Thread thread) { ... }
}
unpark 設為 1,park 消費並歸零park() 不拋出 InterruptedException,需自行檢查 Thread.interrupted()import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.concurrent.locks.LockSupport;
public class CLHBlockingLock {
public static class Node {
volatile Thread thread; // 當前節點關聯的線程
}
@SuppressWarnings("unused")
private volatile Node tail;
private static final ThreadLocal LOCAL = new ThreadLocal<>();
private static final AtomicReferenceFieldUpdater UPDATER =
AtomicReferenceFieldUpdater.newUpdater(CLHBlockingLock.class, Node.class, "tail");
public void lock() {
Node node = new Node();
LOCAL.set(node);
Node pred = UPDATER.getAndSet(this, node); // 原子性入隊
if (pred != null) {
pred.thread = Thread.currentThread(); // 記錄前驅持有者
LockSupport.park(this); // 掛起等待,blocker=this 便於除錯
pred = null; // 助 GC
}
// 獲取鎖成功
}
public void unlock() {
Node node = LOCAL.get();
if (!UPDATER.compareAndSet(this, node, null)) {
// 非尾節點,喚醒後繼者
System.out.println("unpark: " + node.thread.getName());
LockSupport.unpark(node.thread);
}
LOCAL.remove();
}
}
| 維度 | 自旋鎖 | 阻塞鎖 |
|---|---|---|
| CPU 佔用 | 高 (持續自旋) | 低 (釋放 CPU) |
| 上下文切換 | 無 | 2 次 (掛起+喚醒) |
| 延遲 | 極低 (納秒級) | 較高 (微秒~毫秒) |
| 公平性 | 依實作 (通常非公平) | 依佇列實作 (通常公平) |
| 適用臨界區長度 | < 100 納秒 | > 1 微秒 |
| 競爭強度 | 低競爭 | 高競爭 |
JVM synchronized 內建適應性自旋:
-XX:+UseSpinning (預設開)、-XX:PreBlockSpin=10 (自旋次數)public void lock() {
// 1. 短時間自旋嘗試 (適應性)
for (int i = 0; i < SPIN_THRESHOLD; i++) {
if (tryLock()) return;
Thread.onSpinWait();
}
// 2. 自旋失敗,進入阻塞佇列
enqueueAndPark();
}
這正是 ReentrantLock、synchronized (重量級鎖) 的核心策略,平衡低競爭低延遲與高競爭低 CPU 佔用。
synchronized 或 ReentrantLock,JVM 已做極致優化LockSupport.park(blocker) 便於 jstack/VisualVM 診斷阻塞原因ConcurrentHashMap、LongAdder、Atomic*) 替代鎖