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

Java 鎖機制深度解析:自旋鎖與變體實作

     編輯:Java 相關知識

Java 並發鎖核心:自旋鎖原理與三大變體實作

鎖是並發程式設計中保護共享資料一致性的核心機制。Java 提供 synchronized、ReentrantLock 等內建鎖,但理解底層鎖原理 (自旋鎖、票據鎖、CLH 鎖、MCS 鎖) 對於高效能並發元件開發、JUC 原始碼閱讀、無鎖演算法設計至關重要。本文深度解析自旋鎖與三大變體的實作原理、適用場景與效能特徵。

1. 基礎自旋鎖

核心原理

  • 線程嘗試獲取鎖失敗時,不進入阻塞狀態,而是在迴圈中反覆嘗試 (CAS 操作)
  • 避免線程上下文切換開銷,適用於鎖持有時間極短、競爭不激烈的場景
  • 缺點:長時間自旋佔用 CPU、無法保證公平性、可能產生活鎖

CAS 實作 (非公平)

import java.util.concurrent.atomic.AtomicReference;

public class SpinLock {
    // 持有鎖的線程引用,null 表示無鎖
    private final AtomicReference owner = new AtomicReference<>();

    public void lock() {
        Thread current = Thread.currentThread();
        // CAS: 期望值 null → 設為 current
        while (!owner.compareAndSet(null, current)) {
            // 自旋等待,可加入 Thread.onSpinWait() (JDK 9+) 提示 CPU
            Thread.onSpinWait();
        }
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        // 僅持有者能釋放:期望值 current → 設為 null
        owner.compareAndSet(current, null);
    }
}

2. 票據鎖 - 解決公平性

原理

  • 採用「取號、叫號」機制:ticketNum 發號器、serviceNum 服務號
  • 線程取號後自旋等待 myTicket == serviceNum
  • 釋放時 serviceNum++ 喚醒下一位
  • 保證FIFO 公平性,但每次讀取 serviceNum 觸發快取一致性流量
import java.util.concurrent.atomic.AtomicInteger;

public class TicketLock {
    private final AtomicInteger ticketNum = new AtomicInteger(0);
    private final AtomicInteger serviceNum = new AtomicInteger(0);
    private final ThreadLocal myTicket = new ThreadLocal<>();

    public void lock() {
        int ticket = ticketNum.getAndIncrement();
        myTicket.set(ticket);
        while (ticket != serviceNum.get()) {
            Thread.onSpinWait();
        }
    }

    public void unlock() {
        int ticket = myTicket.get();
        // CAS 確保只有持有者能遞增
        serviceNum.compareAndSet(ticket, ticket + 1);
        myTicket.remove();
    }
}

3. CLH 鎖 - 隱式佇列自旋

原理

  • 每個線程建立節點 CLHNode,透過 AtomicReferenceFieldUpdater 原子性插入佇列尾部
  • 線程自旋等待前驅節點的 isLocked 變為 false
  • 釋放時設定自身節點 isLocked = false
  • 優點:無快取行 ping-pong (各線程讀取不同快取行)、簡潔
  • 缺點:NUMA 架構下跨 Socket 讀取前驅節點延遲高
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class CLHLock {
    public static class CLHNode {
        volatile boolean isLocked = true; // 預設鎖定
    }

    @SuppressWarnings("unused")
    private volatile CLHNode tail;
    private static final ThreadLocal LOCAL = new ThreadLocal<>();
    private static final AtomicReferenceFieldUpdater UPDATER =
        AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class, "tail");

    public void lock() {
        CLHNode node = new CLHNode();
        LOCAL.set(node);
        CLHNode pred = UPDATER.getAndSet(this, node); // 原子性入隊
        if (pred != null) {
            while (pred.isLocked) { Thread.onSpinWait(); } // 自旋等待前驅釋放
            pred = null; // 釋放前驅引用助 GC
        }
    }

    public void unlock() {
        CLHNode node = LOCAL.get();
        if (!UPDATER.compareAndSet(this, node, null)) {
            node.isLocked = false; // 非尾節點,直接釋放
        }
        LOCAL.remove();
    }
}

4. MCS 鎖 - 顯式佇列自旋

原理

  • 節點包含 next 指標,形成顯式鏈表
  • 線程自旋等待自身節點的 isLocked (由後繼者設定)
  • 釋放時直接通知後繼者 (next.isLocked = false)
  • 優點:適合 NUMA (本地變數自旋)、無需 AtomicReferenceFieldUpdater
  • 缺點:釋放邏輯較複雜 (需處理 next 尚未設定的競態)
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class MCSLock {
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isLocked = true;
    }

    @SuppressWarnings("unused")
    private volatile MCSNode queue;
    private static final ThreadLocal NODE = new ThreadLocal<>();
    private static final AtomicReferenceFieldUpdater UPDATER =
        AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class, "queue");

    public void lock() {
        MCSNode node = new MCSNode();
        NODE.set(node);
        MCSNode pred = UPDATER.getAndSet(this, node);
        if (pred != null) {
            pred.next = node; // 連接到前驅
            while (node.isLocked) { Thread.onSpinWait(); } // 自旋等待自身釋放
        }
    }

    public void unlock() {
        MCSNode node = NODE.get();
        if (node.next == null) {
            // 可能是尾節點,嘗試 CAS 設為 null
            if (UPDATER.compareAndSet(this, node, null)) {
                return; // 成功,無後繼者
            }
            // 失敗:有後繼者正在設定 next,等待其完成
            while (node.next == null) { Thread.onSpinWait(); }
        }
        // 有後繼者,直接喚醒
        node.next.isLocked = false;
        node.next = null;
        NODE.remove();
    }
}

四種鎖效能特徵對比

鎖類型公平性記憶體佔用快取友好度NUMA 適應性適用場景
基礎自旋鎖非公平極低 (1 個引用)差 (所有 CPU 爭搶同一快取行)差極短臨界區、低競爭
票據鎖公平 (FIFO)低 (2 個整數)差 (所有 CPU 讀寫 serviceNum)差需公平性、短臨界區
CLH 鎖公平 (FIFO)中 (每線程 1 節點)優 (讀取前驅節點,分散快取行)中 (需讀取遠端前驅)通用、中等競爭
MCS 鎖公平 (FIFO)中 (每線程 1 節點)優 (自旋本地變數)優 (本地自旋、遠端只寫入)高競爭、NUMA 系統

JUC 內建鎖關係

  • ReentrantLock (非公平/公平) 底層基於 CLH 變體 (Sync 佇列),但將自旋改為 阻塞 (LockSupport.park/unpark)
  • StampedLock 讀寫鎖使用 CLH 佇列 + 樂觀讀
  • LongAdder、ConcurrentHashMap 等使用 分段/分散式鎖 降低競爭

實務選型建議

  1. 業務代碼:優先使用 synchronized (JVM 優化極好)、ReentrantLock (需可中斷/超時/公平/條件變數)
  2. 高效能組件開發:短臨界區用自旋鎖,高競爭用 MCS/CLH,需公平用票據/CLH/MCS
  3. 避免手寫鎖:除非極特殊場景,JDK 內建鎖經過大量優化 (偏向鎖、輕量級鎖、膨脹、自適應自旋、鎖消除、鎖粗化)
  4. Thread.onSpinWait():JDK 9+ 自旋迴圈必加,提示 CPU 執行 PAUSE 指令降低功耗
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved