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

Java雙重檢查鎖定與延遲初始化:線程安全陷阱與最佳實踐

     編輯:Java 相關知識

高開銷物件初始化常需延遲至首次使用,但線程安全的延遲初始化暗藏陷阱。本文深度剖析雙重檢查鎖定(DCL)的失效根因,提供兩種經實證的正確方案。

問題現場:非線程安全的延遲初始化

public class UnsafeLazyInitialization {
    private static Instance instance;
    public static Instance getInstance() {
        if (instance == null)          // 1: 線程A執行
            instance = new Instance(); // 2: 線程B執行
        return instance;
    }
}

線程A執行檢查時,線程B可能見到instance非null但物件尚未完成初始化。

同步鎖方案:簡單但有性能代價

public class SafeLazyInitialization {
    private static Instance instance;
    public synchronized static Instance getInstance() {
        if (instance == null) instance = new Instance();
        return instance;
    }
}

synchronized導致性能開銷。若getInstance()高頻調用,吞吐量下降;低頻場景可接受。

雙重檢查鎖定(DCL):看似完美實則失效的「聰明」優化

public class DoubleCheckedLocking {
    private static Instance instance;
    public static Instance getInstance() {
        if (instance == null) {              // 4: 第一次檢查
            synchronized (DoubleCheckedLocking.class) { // 5: 加鎖
                if (instance == null)        // 6: 第二次檢查
                    instance = new Instance(); // 7: 問題根源
            }
        }
        return instance;
    }
}

表面優勢:物件創建後無需獲取鎖直接返回。但第7行可能發生指令重排序,導致其他線程見到半初始化物件。

根源深度解析:指令重排序與Intra-thread Semantics

instance = new Instance()分解為三步偽代碼:

memory = allocate();        // 1: 分配記憶體
ctorInstance(memory);       // 2: 初始化物件
instance = memory;          // 3: 設置引用指向記憶體

JIT編譯器可能重排序為1→3→2:

memory = allocate();
instance = memory;          // 引用已指向,但物件未初始化!
ctorInstance(memory);

依據《Java語言規範》,單線程內重排序不得改變執行結果(intra-thread semantics)。上圖藍線顯示:只要2排在4前面,單線程語義滿足。

但多線程並發時(紅線虛箭頭標示錯誤讀取):

線程A重排序後,線程B在第4行判斷instance != null,隨後訪問未初始化物件。

兩大解決方案思路

  1. 禁止2、3重排序:用volatile語義阻擋
  2. 允許重排序但不讓其他線程「看見」:利用類初始化鎖機制

方案一:volatile雙重檢查鎖定(JDK 5+)

public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;  // 關鍵:volatile
    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                    instance = new Instance();  // volatile禁止重排序
            }
        }
        return instance;
    }
}

JDK 5新記憶體模型(JSR-133)增強volatile語義,禁止構造器寫入與引用賦值重排序。執行時序:

方案二:靜態內部類延遲初始化(Initialization On Demand Holder)

public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }
    public static Instance getInstance() {
        return InstanceHolder.instance;  // 觸發類初始化
    }
}

JVM類初始化階段獲取唯一初始化鎖(LC),多線程並發初始化時自動同步:

  • 線程A獲取鎖執行初始化,線程B在condition等待
  • 線程A完成初始化,喚醒等待線程
  • 後續線程獲取鎖即可見到初始化結果(happens-before關係)

類初始化鎖的happens-before保證:初始化線程的寫入操作,後續線程必定可見。

方案對比與選擇指南

方案優勢適用場景
volatile DCL支援實例欄位與靜態欄位延遲初始化需對實例欄位線程安全延遲初始化
靜態內部類代碼更簡潔,無需額外同步關鍵字需對靜態欄位線程安全延遲初始化

總結

延遲初始化降低啟動開銷但增加訪問成本。多數情況正常初始化優於延遲初始化。確需使用時:

  • 實例欄位 → volatile雙重檢查鎖定
  • 靜態欄位 → 靜態內部類模式

作者:zuoxiaolong(左潇龍)
出處:博客園左潇龍技術博客

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