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

Java 多線程同步核心:鎖機制、synchronized 鎖升級與 JMM 內存語義深度解析

     編輯:Java 相關知識

從「一把鑰匙」到「鎖升級」:Java 同步機制的演進與本質

synchronized 是 Java 並發編程的基石。本文用「房子-房間-鑰匙」隱喻解構監視器鎖模型,深度剖析鎖升級 (偏向/輕量/重量)、JMM 內存語義、鎖消除/粗化等 JVM 優化技術。

監視器鎖隱喻模型:一把鑰匙管所有上鎖房間

  • Object (對象) = 一棟大房子,大門敞開
  • synchronized 方法/塊 = 上鎖的房間
  • 普通方法 = 不上鎖的房間,隨意進出
  • 鎖 (Monitor) = 房門口掛的一把鑰匙
  • 執行緒 = 想進房間的人

核心規則: 想進上鎖房間 → 必須拿到鑰匙 → 用完立即還鑰匙 (「隨用隨借,用完即還」)。同一時間只能有一人持有鑰匙。

同步代碼塊 vs 同步方法:鎖對象與粒度差異


public class SyncDemo {
    private final Object lock = new Object(); // 顯式鎖對象
    
    // 1. 同步實例方法:鎖對象 = this (當前實例)
    public synchronized void instanceMethod() { ... }
    
    // 2. 同步靜態方法:鎖對象 = Class 對象 (SyncDemo.class)
    public static synchronized void staticMethod() { ... }
    
    // 3. 同步代碼塊:可指定任意鎖對象,粒度最細
    public void blockSync() {
        synchronized (lock) { // 鎖住 lock 對象,不影響 this 其他同步方法
            // 臨界區代碼
        }
        synchronized (OtherClass.class) { // 甚至可鎖其他類 Class 對象 (跨類同步)
            // 另一個臨界區
        }
    }
}
維度同步方法同步代碼塊
鎖對象隱性 (this / Class)顯性指定 (任意對象)
鎖粒度整個方法可精確到幾行代碼
靈活性低高 (可跨對象/類同步)
效能影響大 (鎖住整方法)小 (僅鎖臨界區)
死鎖風險較高 (this 易被外部鎖)可控 (私有 final lock 對象)

synchronized 鎖升級演進:JVM 為效能做的極致優化

Java 6 引入鎖升級機制,從無鎖 → 偏向鎖 → 輕量級鎖 → 重量級鎖,單向升級,不可降級。

鎖狀態適用場景核心機制優缺點標記位 (Mark Word)
無鎖無並發訪問無同步開銷最快,但無安全保障01 (無鎖標記)
偏向鎖單執行緒重複訪問CAS 設置 ThreadID 到 Mark Word,後續進出無 CAS極快 (無 CAS),但撤銷成本高01 (偏向鎖標記) + ThreadID
輕量級鎖多執行緒交替訪問 (低競爭)CAS 將 Mark Word 複製到棧幀 Lock Record,CAS 成功即獲鎖無內核態切換,自旋等待00 (輕量級鎖標記) + 指向 Lock Record
重量級鎖高競爭、長臨界區ObjectMonitor 監視器,OS 互斥量,阻塞掛起效能最低,但公平、可靠10 (重量級鎖標記) + 指向 ObjectMonitor

Mark Word 結構演變 (64位 JVM,壓縮指針開啟)


// 無鎖狀態
| 25bit 哈希碼 | 4bit 分代年齡 | 1bit 無鎖標記(0) | 2bit 鎖標記(01) |

// 偏向鎖狀態
| 23bit ThreadID | 2bit Epoch | 4bit 分代年齡 | 1bit 偏向標記(1) | 2bit 鎖標記(01) |

// 輕量級鎖狀態
| 62bit 指向棧幀 Lock Record 的指針 | 2bit 鎖標記(00) |

// 重量級鎖狀態
| 62bit 指向 ObjectMonitor 的指針 | 2bit 鎖標記(10) |

JMM 內存語義:synchronized 的 happens-before 保證

  • 解鎖 Happens-Before 加鎖: 線程 A 解鎖前的寫入,對後續加鎖的線程 B 可見
  • 效果等價於 volatile 讀寫: 進入同步塊 = volatile 讀 (刷新本地緩存);退出同步塊 = volatile 寫 (刷新到主存)
  • 禁止指令重排: 同步塊內代碼不會被重排到塊外,塊外代碼不會重排到塊內

sleep() 與鎖的關係:睡覺不還鑰匙


synchronized (lock) {
    // 持有鎖
    Thread.sleep(1000); // 睡眠期間 **繼續持有鎖**!其他線程無法進入
    // 做完業務
} // 這裡才釋放鎖

原因: sleep() 不釋放監視器鎖,只有 wait() 會釋放鎖並進入等待隊列。

JVM 進階優化:鎖消除、鎖粗化、適應性自旋

  • 鎖消除 (Escape Analysis): JIT 編譯器發現鎖對象未逃逸出線程 (如局部 new Object()),直接移除同步
  • 鎖粗化: 檢測到同一鎖連續多次加釋放 (如循環內 synchronized),合併為一次大鎖
  • 適應性自旋: 輕量級鎖自旋次數根據歷史成功率動態調整,避免無效自旋浪費 CPU

最佳實踐總結

  • ✅ 優先用同步代碼塊 + 私有 final lock 對象,縮小臨界區,避免死鎖
  • ✅ 避免鎖住 this / 公開對象,外部代碼可能惡意鎖住導致死鎖
  • ✅ 臨界區極短、競爭低 → 信任 JVM 鎖升級,無需手動優化
  • ✅ 高競爭、長臨界區 → 考慮 ReentrantLock (公平鎖、可中斷、Condition)
  • ✅ 只讀共享數據 → 用 StampedLock 樂觀讀 / CopyOnWriteArrayList
  • ❌ 不要在同步塊調用睡眠/IO/外部服務,長時間持鎖殺死併發度

理解鎖升級、Mark Word、JMM 語義,才能在高併發系統中寫出既正確又高效的同步代碼。

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