首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JUC 公平鎖獲取機制全景解析:從 lock() 到 CLH 隊列調度的完整鏈路

     編輯:Java 相關知識

Java 並發包(JUC)中的 ReentrantLock 是構建高併發系統的基石。其公平鎖實現基於 AQS(AbstractQueuedSynchronizer)框架,通過 CLH 隊列實現嚴格的先來先得(FIFO)調度。本文基於 JDK 1.7.0_40 源碼,從 lock() 入口出發,層層剖析 acquire() → tryAcquire() → addWaiter() → acquireQueued() 的完整獲取鏈路,還原公平鎖「不插隊、按序等待、精準喚醒」的核心邏輯。

核心類結構:ReentrantLock、Sync、AQS 三層架構

ReentrantLock (實現 Lock 接口)
    └─ sync: Sync (組合關係,核心同步器)
           ├─ FairSync (公平鎖實現)
           └─ NonfairSync (非公平鎖實現)
                └─ AbstractQueuedSynchronizer (AQS,模板方法框架)
                     └─ AbstractOwnableSynchronizer (持有 exclusiveOwnerThread)
  • ReentrantLock 通過構造函數注入 FairSync 或 NonfairSync 決定公平性
  • Sync 繼承 AQS,封裝鎖狀態(state)、持有者線程(exclusiveOwnerThread)及核心操作
  • AQS 提供模板方法(acquire/release),子類實現 tryAcquire/tryRelease 定制語義

獲取鎖四大核心步驟

1. lock() → acquire(1):入口與參數含義

// FairSync.lock()
final void lock() {
    acquire(1);
}

// AQS.acquire()
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • 參數 1 代表「獲取一個鎖許可」。可重入鎖中,state 表示重入次數:0=空閒,1=首次獲取,2=重入一次...
  • 流程:先嘗試快速獲取(tryAcquire)→ 失敗則入隊等待(addWaiter)→ 隊列中調度獲取(acquireQueued)

2. tryAcquire(int acquires):公平性核心判邏輯

// FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {                                    // 鎖空閒
        if (!hasQueuedPredecessors() &&              // 【關鍵】無前驅等待者
            compareAndSetState(0, acquires)) {       // CAS 設置狀態
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) { // 重入
        int nextc = c + acquires;
        if (nextc < 0) throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
  • 公平性關鍵:!hasQueuedPredecessors() 確保只有在「無前驅等待線程」時才嘗試 CAS 獲取,杜絕插隊
  • 重入支持:持有者再次獲取直接疊加 state,無需進隊列
  • compareAndSetState 基於 Unsafe.CAS,保證狀態更新原子性

3. hasQueuedPredecessors():判斷是否有前驅等待者

public final boolean hasQueuedPredecessors() {
    Node t = tail, h = head, s;
    return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
  • head != tail:隊列非空
  • h.next == null:頭節點尚未關聯後繼(初始化瞬間)
  • h.next.thread != current:後繼節點不是當前線程
  • 以上任一為真,即存在前驅等待者,當前線程必須入隊排隊

4. addWaiter(Node.EXCLUSIVE):入隊 CLH 隊列

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode); // 創建節點,模式=獨占
    Node pred = tail;
    if (pred != null) {                      // 隊列非空:快速路徑
        node.prev = pred;
        if (compareAndSetTail(pred, node)) { // CAS 設置尾節點
            pred.next = node;
            return node;
        }
    }
    enq(node);                               // 隊列空或 CAS 失敗:完整入隊邏輯
    return node;
}

private Node enq(final Node node) {
    for (;;) {                               // 自旋直到入隊成功
        Node t = tail;
        if (t == null) {                     // 隊列未初始化
            if (compareAndSetHead(new Node())) // 創建哨兵頭節點
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) { // CAS 更新尾節點
                t.next = node;
                return t;
            }
        }
    }
}
  • CLH 隊列變體:雙向鏈表,head 為哨兵節點(不關聯線程),tail 指向最後等待者
  • 快速路徑 + 自旋兜底:先嘗試 CAS 直接接在尾部,失敗進 enq() 自旋保證最終入隊
  • Node.EXCLUSIVE 標記獨占模式,nextWaiter=null

5. acquireQueued(Node, int):隊列中調度獲取鎖

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor(); // 獲取前驅節點
            if (p == head && tryAcquire(arg)) { // 前驅是頭節點且嘗試獲取成功
                setHead(node);                  // 當前節點成為新頭節點
                p.next = null;                  // 斷開舊頭節點引用(助 GC)
                failed = false;
                return interrupted;             // 返回是否被中斷過
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;             // 記錄中斷狀態
        }
    } finally {
        if (failed) cancelAcquire(node);       // 異常退出取消節點
    }
}
  • 自旋 + 休眠:循環中不斷嘗試,條件滿足則獲取鎖並返回
  • 公平性保障:p == head 確保只有前驅釋放鎖並喚醒後,當前節點才嘗試獲取,嚴格 FIFO
  • setHead(node) 將當前節點設置為頭節點(哨兵),原節點斷開引用

6. shouldParkAfterFailedAcquire():判斷是否應阻塞

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL) return true;        // 前驅已發出喚醒信號,可安全阻塞
    if (ws > 0) {                              // 前驅已取消
        do { node.prev = pred = pred.prev; }   // 向前跳過取消節點
        while (pred.waitStatus > 0);
        pred.next = node;
    } else {                                   // ws == 0 或 PROPAGATE
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 設置前驅為 SIGNAL
    }
    return false;                              // 本輪不阻塞,下輪重試
}
  • 三種情況:
    1. SIGNAL:前驅已準備好喚醒後繼,返回 true → 阻塞
    2. CANCELLED (>0):前驅取消,跳過取消節點重新鏈接,返回 false → 下輪重試
    3. 0/PROPAGATE:CAS 設置前驅為 SIGNAL,返回 false → 下輪重試
  • 這機制確保:只有前驅真正準備好喚醒時,當前線程才進入阻塞,避免虛假喚醒

7. parkAndCheckInterrupt():阻塞與中斷檢查

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);                    // 阻塞當前線程
    return Thread.interrupted();               // 返回並清除中斷狀態
}
  • 使用 LockSupport.park() 而非 Object.wait(),無需持有監視器鎖,輕量級
  • 喚醒來源:前驅 unpark() 或 其他線程 interrupt()
  • Thread.interrupted() 返回中斷狀態並清除標誌,故需在外層記錄

8. selfInterrupt():中斷補償機制

private static void selfInterrupt() {
    Thread.currentThread().interrupt();        // 重新設置中斷標誌
}
  • 為什麼需要? 在 acquireQueued 中,若線程被中斷喚醒但尚未獲取鎖(因前驅未釋放),中斷標誌被 interrupted() 清除。最終獲取鎖後,調用 selfInterrupt() 恢復中斷狀態,保證上層代碼能感知中斷
  • 體現「中斷不丟失」原則:中斷是線程協作信號,不應因鎖競爭而丟失

CLH 隊列節點(Node)結構詳解

static final class Node {
    volatile int waitStatus;   // 節點狀態:CANCELLED(1), SIGNAL(-1), CONDITION(-2), PROPAGATE(-3), 0
    volatile Node prev;        // 前驅節點
    volatile Node next;        // 後繼節點
    volatile Thread thread;    // 關聯線程
    Node nextWaiter;           // 條件隊列指針 / 模式標記 (SHARED/EXCLUSIVE)
    
    final boolean isShared() { return nextWaiter == SHARED; }
    final Node predecessor() { Node p = prev; if (p==null) throw new NPE(); return p; }
}

完整獲取流程時序圖

Thread A (新請求)                    CLH 隊列 (Head=H, Tail=T)          Thread B (持有鎖)
    │                                  │                                  │
    ├─ lock() → acquire(1)             │                                  │
    ├─ tryAcquire(1) → false           │                                  │
    ├─ addWaiter(EXCLUSIVE)            │                                  │
    │   └─ 節點A入隊: H ↔ ... ↔ T ↔ A  │                                  │
    ├─ acquireQueued(A)                │                                  │
    │   ├─ p = A.prev (T)              │                                  │
    │   ├─ p != head → shouldPark...   │                                  │
    │   │   └─ 設置 T.waitStatus=SIGNAL│                                  │
    │   ├─ parkAndCheckInterrupt()     │                                  │
    │   │   └─ LockSupport.park() → 阻塞                            │
    │                                  │                                  │
    │                                  │              Thread B 釋放鎖      │
    │                                  │              ├─ unlock()          │
    │                                  │              ├─ release(1)        │
    │                                  │              ├─ tryRelease→true   │
    │                                  │              ├─ unparkSuccessor(H)│
    │                                  │              │   └─ unpark(A)     │
    │                                  │              │                    │
    │   ├─ park() 返回                 │                                  │
    │   ├─ p = A.prev (H)              │                                  │
    │   ├─ p == head && tryAcquire→true│                                  │
    │   ├─ setHead(A)  // A 成為新頭    │                                  │
    │   └─ 獲得鎖,返回                │                                  │

設計精髓總結

  1. 模板方法 + 狀態機:AQS 定義骨架(acquire/release),子類實現語義(tryAcquire/tryRelease),waitStatus 驅動協作
  2. CLH 隊列變體:雙向鏈表 + 哨兵頭節點,prev/next 支持 O(1) 入隊、出隊與取消節點清理
  3. 公平性核心:hasQueuedPredecessors() + p == head 雙重把關,嚴格 FIFO,犧牲吞吐換取無飢餓
  4. 中斷語義保持:park/interrupted/selfInterrupt 三部曲,確保中斷信號不丟失、不誤觸發
  5. LockSupport 原語:替代傳統 wait/notify,無需監視器,語義更清晰(許可證模型)

公平鎖獲取流程雖代碼量大,但邏輯清晰:嘗試快速路徑 → 入隊排隊 → 自旋檢查前驅狀態 → 阻塞等待喚醒 → 獲取鎖更新頭節點。掌握這一流程,不僅能讀懂 JUC 所有同步器(Semaphore、CountDownLatch、ReentrantReadWriteLock)的底層邏輯,更能在高併發系統調優、死鎖排查、自定義同步器開發中得心應手。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved