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

Java 單例模式線程安全實戰:靜態內部類 vs 枚舉 vs DCL 深度對比

     編輯:Java 相關知識

單例模式看似簡單,卻是面試高頻考點與生產環境 Bug 重災區。本文對比三種主流線程安全實現——靜態內部類、枚舉、雙重檢查鎖 (DCL),從類加載機制、指令重排、序列化破壞、反射攻擊四個維度深度剖析,給出選型建議。

方案一:靜態內部類 (推薦通用首選)

public class Singleton {
    private Singleton() {} // 私有建構子

    private static class Holder {
        static final Singleton INSTANCE = new Singleton(); // 類加載時初始化
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE; // 首次呼叫觸發 Holder 類加載
    }

    public void doSomething() { System.out.println("Singleton working..."); }
}

原理:JVM 保證類加載過程的原子性與可見性。Holder 類只有在首次呼叫 getInstance() 時才會被加載、初始化 INSTANCE,天然具備**延遲載入**與**線程安全**。

方案二:枚舉單例 (Effective Java 作者 Josh Bloch 強推)

public enum Singleton {
    INSTANCE;

    public void doSomething() { System.out.println("Enum singleton working..."); }
}

核心優勢:

  • 語法簡潔,零樣板代碼
  • **天然防反射攻擊**:Constructor.newInstance() 會拋異常
  • **天然防序列化破壞**:JVM 保證枚舉常量單例,無需 readResolve
  • 類加載機制同樣保證線程安全與延遲初始化

方案三:雙重檢查鎖 (DCL) — 必須加 volatile

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

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次檢查:無鎖快速路徑
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次檢查:同步區內確認
                    instance = new Singleton(); // 非原子操作:分配記憶體 → 初始化 → 賦值引用
                }
            }
        }
        return instance;
    }
}

為何要 volatile:new Singleton() 非原子,JIT 可能重排為「分配記憶體 → 賦值引用 → 初始化」。無 volatile 時,其他線程可能看到「引用非 null 但物件未初始化」的半成品。volatile 禁止重排,確保發布安全。

四大維度深度對比

維度靜態內部類枚舉DCL + volatile
線程安全✅ 類加載鎖✅ 類加載鎖✅ volatile + synchronized
延遲載入✅✅✅
防反射❌ 可暴力破解✅ JVM 禁止❌ 可暴力破解
防序列化破壞❌ 需手寫 readResolve✅ JVM 保證❌ 需手寫 readResolve
代碼複雜度低極低中 (易寫錯)
適用場景通用單例、需繼承類通用單例、安全要求高需精確控制初始化時機、或舊代碼維護

反射破解示範 (非枚舉)

Constructor c = Singleton.class.getDeclaredConstructor();
c.setAccessible(true);
Singleton s1 = c.newInstance(); // 繞過私有建構子
Singleton s2 = c.newInstance();
System.out.println(s1 == s2); // false!單例被破壞

枚舉嘗試同樣代碼會拋 IllegalArgumentException: Cannot reflectively create enum objects。

序列化破壞與修復

非枚舉單例實作 Serializable 後,反序列化會產生新實例。修復:

// 單例類內新增
private Object readResolve() { return INSTANCE; } // 或 getInstance()

選型建議決策樹

  1. **首選枚舉** — 簡潔、安全、防反射/序列化破壞,無理由不選
  2. 單例需**繼承其他類**或**實現介面** — 用靜態內部類 (枚舉不可繼承)
  3. 維護**舊代碼**已用 DCL — 確認 volatile 在位,補上 readResolve
  4. 極罕見:需**精確控制初始化時機** (如依賴外部配置尚未載入) — DCL 允許在同步塊前做額外檢查

結語

單例模式的本質是「控制實例化數量 = 1」。枚舉以語言級保證最徹底解決此問題,應成為現代 Java 開發的預設選擇。下次面試或 Code Review 遇到單例,請直接問:「為何不用 enum?」

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