Java 鎖升級機制詳解

引言

最近有個三年左右的兄弟面試java 被問到這樣一道經典的八股文面試題: 你講講java里面的鎖升級? 他感覺回答的不是很好,然后回去找資料學習了一波,然后下面是他輸出的文章,希望對找工作的其他朋友也有些幫助。

1. 概述

Java 的鎖升級機制是 JVM 在 JDK 1.6 后引入的重要優化策略,目的是在多線程環境下平衡 線程安全性能開銷。通過動態調整鎖的復雜度,JVM 根據競爭強度逐步升級鎖的狀態,避免在低競爭場景下使用高成本的重量級鎖。

2. 鎖類型及特點

鎖類型 適用場景 性能開銷 核心機制
無鎖(Unlocked) 無線程競爭 極低 直接通過 CAS 操作嘗試獲取鎖。
偏向鎖(Biased Locking) 單線程重復訪問(無競爭) 極低 對象頭記錄偏向線程 ID,后續同一線程無需競爭,直接獲取鎖。
輕量級鎖(Lightweight Lock) 低競爭(多個線程交替訪問) 中等 通過 CAS 自旋嘗試獲取鎖,避免操作系統級別的阻塞。
重量級鎖(Heavyweight Lock) 高競爭(長時間阻塞或高并發) 依賴操作系統互斥量(Mutex),線程被掛起并排隊等待。

3. 鎖升級的過程

鎖升級路徑為:無鎖 → 偏向鎖 → 輕量級鎖 → 重量級鎖,且 不可逆(只能升級,不能降級)。

3.1 無鎖 → 偏向鎖

  • 觸發條件:第一個線程訪問同步代碼塊時。
  • 過程
    1. JVM 通過 CAS 操作將對象頭的 Mark Word 標記為偏向鎖。
    2. 記錄當前線程 ID 和偏向時間戳。
    3. 后續同一線程再次訪問時,直接通過比對線程 ID 獲取鎖(無需 CAS 操作)。

3.2 偏向鎖 → 輕量級鎖

  • 觸發條件:第二個線程嘗試獲取同一鎖(出現競爭)。
  • 過程
    1. 偏向鎖失效,JVM 撤銷偏向鎖(可能觸發 STW,Stop-The-World)。
    2. 線程通過自旋(Spin)和 CAS 操作嘗試獲取鎖。
    3. 若自旋成功,則升級為輕量級鎖;否則繼續自旋或升級為重量級鎖。

3.3 輕量級鎖 → 重量級鎖

  • 觸發條件
    • 自旋次數超過閾值(默認 10 次,可通過 -XX:PreBlockSpin 調整)。
    • 多個線程同時競爭鎖(如第三個線程加入競爭)。
  • 過程
    1. JVM 將鎖升級為重量級鎖,對象頭指向監視器(Monitor)。
    2. 線程進入操作系統內核態的阻塞隊列,等待調度器喚醒。
    3. 未獲取鎖的線程通過 ObjectMonitor 等待喚醒。

4. 鎖升級的優缺點

4.1 優點

  1. 減少無競爭場景的開銷:偏向鎖和輕量級鎖避免了頻繁的 CAS 和上下文切換。
  2. 動態適配競爭強度:在低競爭時保持高性能,在高競爭時保證線程安全。

4.2 缺點

  1. 偏向鎖撤銷開銷:當其他線程競爭時,撤銷偏向鎖會導致 STW,影響性能。
  2. 重量級鎖的高開銷:在高競爭場景下,頻繁的線程阻塞/喚醒會顯著降低性能。

5. 鎖升級的優化策略

5.1 減少鎖持有時間

  • 優化方向:縮短同步代碼塊的執行時間,降低鎖的競爭概率。
  • 示例
    // 不推薦:鎖持有時間過長
    synchronized (lock) {
        // 復雜計算或 IO 操作
    }
    
    // 推薦:僅在關鍵代碼塊加鎖
    int result = doSomeComputation(); // 非同步操作
    synchronized (lock) {
        sharedVariable = result;
    }
    

5.2 使用分段鎖(Segment Locking)

  • 優化方向:將一個大鎖拆分為多個小鎖,減少鎖的競爭范圍。
  • 示例ConcurrentHashMap 使用分段鎖(JDK 8 后改為 CAS + synchronized)。

5.3 禁用偏向鎖

  • 適用場景:頻繁切換線程的場景(如高并發服務)。
  • JVM 參數
    -XX:-UseBiasedLocking  # 禁用偏向鎖
    

5.4 調整自旋次數

  • 適用場景:輕量級鎖的自旋可能因 CPU 空閑而浪費資源。
  • JVM 參數
    -XX:PreBlockSpin=5  # 設置自旋次數為 5
    

6. 代碼示例

public class LockUpgradeExample {
    private final Object lock = new Object();

    public void performTask() {
        synchronized (lock) {
            // 同步代碼塊
        }
    }

    public static void main(String[] args) {
        LockUpgradeExample example = new LockUpgradeExample();
        Thread t1 = new Thread(example::performTask);
        Thread t2 = new Thread(example::performTask);

        t1.start(); // 初始為偏向鎖(t1)
        t2.start(); // 觸發偏向鎖撤銷,升級為輕量級鎖
    }
}

7. 關鍵 JVM 參數

參數 作用
-XX:+UseBiasedLocking 開啟/關閉偏向鎖(默認開啟,Java 15+ 默認關閉)。
-XX:BiasedLockingStartupDelay=0 立即啟用偏向鎖(避免延遲)。
-XX:PreBlockSpin 設置輕量級鎖自旋次數(默認 10)。
-XX:-UseSpinning 關閉自旋鎖(強制進入重量級鎖)。

8. 總結

  • 鎖升級是 JVM 自動管理的機制,開發者無需手動干預,但理解其原理有助于優化并發性能。
  • 偏向鎖適合單線程場景,輕量級鎖適合低競爭場景,重量級鎖適合高競爭場景。
  • 鎖升級不可逆,一旦升級到重量級鎖,后續操作將始終使用重量級鎖。

通過合理設計代碼(如減少鎖粒度、避免過早膨脹到重量級鎖),可以最大化 Java 的并發性能。