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

Java 鎖機制:阻塞鎖原理與 CLH 變體實作

     編輯:Java 相關知識

Java 並發鎖進階:阻塞鎖原理與 LockSupport 實作

與自旋鎖「佔用 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) → 獲取鎖成功

LockSupport 核心 API

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) { ... }
}

許可證機制關鍵特性

  • 每個線程關聯一個「許可證」 (0 或 1),unpark 設為 1,park 消費並歸零
  • unpark 可先於 park 呼叫:許可證預存,後續 park 立即返回,不阻塞
  • 避免「喚醒遺失」競態:線程 A 呼叫 unpark(B) → B 尚未 park → B 之後 park 直接通過
  • 不響應中斷:park() 不拋出 InterruptedException,需自行檢查 Thread.interrupted()

CLH 阻塞鎖實作

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();
    }
}

自旋鎖 vs 阻塞鎖 選型指南

維度自旋鎖阻塞鎖
CPU 佔用高 (持續自旋)低 (釋放 CPU)
上下文切換無2 次 (掛起+喚醒)
延遲極低 (納秒級)較高 (微秒~毫秒)
公平性依實作 (通常非公平)依佇列實作 (通常公平)
適用臨界區長度< 100 納秒> 1 微秒
競爭強度低競爭高競爭

適應性自旋鎖 (Adaptive Spinning)

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 佔用。

生產環境建議

  1. 業務代碼直接用 synchronized 或 ReentrantLock,JVM 已做極致優化
  2. 高效能組件開發才考慮手寫鎖,且必須經過 JMH 基準測試驗證
  3. 使用 LockSupport.park(blocker) 便於 jstack/VisualVM 診斷阻塞原因
  4. 避免在鎖內執行 I/O、睡眠、長時間運算,縮小臨界區
  5. 考慮無鎖資料結構 (ConcurrentHashMap、LongAdder、Atomic*) 替代鎖
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved