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

單例模式演進史:從懶漢式到雙重檢查鎖定的陷阱與終極修正

     編輯:Java 相關知識

單例模式看似簡單,實則暗藏多線程並發陷阱。本文層層遞進分析五個版本:基本懶漢、同步方法、同步塊、雙重檢查鎖定 (DCL)、修正版 DCL,並用字節碼證明 DCL 在 JMM (Java Memory Model) 下的失效場景,給出生產可用的線程安全單例寫法。

版本演進對比

版本代碼關鍵點問題
1. 基本懶漢if (instance==null) instance=new Singleton();多線程競態,可能創建多實例
2. 同步方法synchronized static getInstance()每次調用鎖開銷大,效能差
3. 同步塊if(null) synchronized{ instance=new... }同版本 1,同步塊外無檢查,仍有競態
4. 雙重檢查鎖定 (DCL)if(null) synchronized{ if(null) instance=new... }指令重排導致半初始化物件洩漏
5. 修正版 DCL局部變量 inst + 兩次同步塊修復重排問題,效能與安全兼得

DCL 失效的字節碼級證據

new Singleton() 非原子操作,JIT 編譯器可能重排為:

mem = allocate();        // 1. 分配記憶體
instance = mem;          // 2. 引用賦值(此時 instance != null)
ctorSingleton(instance); // 3. 執行構造函數初始化

線程 A 執行到步驟 2 被搶佔,線程 B 見 instance != null 直接返回,**拿到未初始化完成的物件** → NPE 或數據錯誤。

修正版 DCL 代碼 (JDK 5+ volatile 語義保證)

public class Singleton {
    private static volatile Singleton instance;  // volatile 禁止指令重排

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                  // 第一次檢查(無鎖)
            synchronized (Singleton.class) {
                if (instance == null) {          // 第二次檢查(鎖內)
                    instance = new Singleton();  // 唯一初始化點
                }
            }
        }
        return instance;
    }
}

關鍵點:volatile 確保 instance = new Singleton() 的「寫入引用」發生在「構造函數完成」之後,對其他線程可見。

現代更優替代方案

靜態內部類 (Initialization-on-demand Holder Idiom)

public class Singleton {
    private Singleton() {}
    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}
  • 利用類加載機制天然線程安全、無鎖、延遲初始化、代碼極簡。
  • 《Effective Java》推薦寫法。

枚舉單例 (Enum Singleton)

public enum Singleton {
    INSTANCE;
    public void doSomething() {}
}
  • 防反射破壞、防序列化重建、防克隆、天然線程安全。
  • Josh Bloch 強力推薦,**最佳單例實現**。

總結建議

  1. 新代碼**首選枚舉單例**,次選靜態內部類。
  2. 維護舊 DCL 代碼,**務必加上 volatile**。
  3. 避免使用雙重檢查鎖定的變體(如局部變量雙同步),易出錯且難維護。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved