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

雙重檢查鎖 (DCL) 與延遲初始化:volatile 修復與靜態內部類方案深度解析

     編輯:Java 綜合教學

問題場景:單例模式延遲初始化

// 非線程安全
public class Singleton {
    private static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) instance = new Singleton();
        return instance;
    }
}

多線程並發調用 getInstance() 可能創建多個實例。

同步方法版:性能瓶頸

public static synchronized Singleton getInstance() {
    if (instance == null) instance = new Singleton();
    return instance;
}

每次調用都加鎖,單例創建後仍同步,吞吐量極低。

雙重檢查鎖 (DCL) 最初版:看似完美實則有缺陷

public static Singleton getInstance() {
    if (instance == null) {                 // 1. 第一次檢查
        synchronized (Singleton.class) {    // 2. 加鎖
            if (instance == null) {         // 3. 第二次檢查
                instance = new Singleton(); // 4. 初始化
            }
        }
    }
    return instance;
}

缺陷根源:instance = new Singleton() 非原子操作,JVM 可能重排序為:

  1. memory = allocate() 分配內存
  2. instance = memory 引用指向內存 (對象尚未初始化!)
  3. ctorInstance(memory) 執行構造函數初始化

線程 A 執行到步驟 2 時,線程 B 進入第一次檢查看到 instance != null,直接返回未初始化完成的對象 → 空指針/數據不一致。

方案一:volatile 禁止重排序 (JDK 5+)

private static volatile Singleton instance; // 關鍵:volatile

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton(); // volatile 保證:寫入 instance 前構造函數必須完成
            }
        }
    }
    return instance;
}

原理:volatile 的 寫屏障 禁止指令重排序,確保「構造函數執行完成」 happens-before 「引用賦值給 instance」,其他線程讀取到非 null 時對象必然完全初始化。

方案二:靜態內部類 (Initialization On Demand Holder Idiom) —— 推薦

public class Singleton {
    private Singleton() {}
    private static class Holder {
        static final Singleton INSTANCE = new Singleton(); // 類加載時初始化,JVM 保證線程安全
    }
    public static Singleton getInstance() {
        return Holder.INSTANCE; // 首次調用觸發 Holder 類加載
    }
}

優勢:

  • 無鎖、高性能、代碼簡潔。
  • 利用 類加載機制:JVM 在類初始化階段獲取 初始化鎖,多線程並發初始化同一類時會阻塞等待,天然線程安全。
  • 實現 延遲加載:Singleton 類加載時不初始化 INSTANCE,只有調用 getInstance() 訪問 Holder.INSTANCE 時才觸發 Holder 初始化。

類初始化線程安全機制深度剖析 (JLS §12.4)

  1. 獲取初始化鎖:線程嘗試獲取 Class 對象關聯的鎖,獲取者繼續,其他線程等待。
  2. 執行初始化:持鎖線程執行 <clinit>() (靜態字段賦值、靜態代碼塊),期間其他線程在 Condition 上等待。
  3. 發布結果:初始化完成設置狀態 initialized,喚醒等待線程。
  4. 後續訪問:喚醒線程讀取到 initialized 直接返回;新線程獲取鎖讀到 initialized 直接返回。

根據 JMM 鎖規則:unlock → lock 構成 happens-before,保證初始化線程的寫入 (對象構造) 對後續線程可見。

方案對比與選型建議

方案適用場景優點缺點
synchronized 方法低併發、初始化極快簡單性能差
volatile DCL實例字段延遲初始化、需參數構造支持實例字段、靈活代碼較複雜、易寫錯
靜態內部類靜態字段單例、無參構造無鎖、JVM 保證安全、代碼極簡不支持實例字段、構造需無參

總結

  1. 延遲初始化大多數情況下 不必要,直接靜態初始化或急切初始化更簡單安全。
  2. 必須延遲初始化靜態字段 → 靜態內部類 (首選)。
  3. 必須延遲初始化實例字段 → volatile DCL (唯一選擇)。
  4. 理解 volatile 寫屏障、類初始化鎖、JMM happens-before 是掌握並發編程的核心鑰匙。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved