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

Java 單例模式演進:從餓漢到雙重檢查鎖的線程安全優化之路

     編輯:Java 相關知識

單例模式核心意圖

確保一個類全局僅有一個實例,並提供全局訪問點。構造器私有化,靜態工廠方法返回唯一實例。

五種寫法演進對比

1. 餓漢式 (類加載時創建)

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() { return INSTANCE; }
}
  • ✅ 線程安全(類加載機制保證)
  • ✅ 實現最簡單
  • ❌ 無延遲加載:類加載即創建,即使從未使用也佔用內存

2. 懶漢式 (首次調用創建) — 非線程安全

public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) instance = new Singleton();
        return instance;
    }
}
  • ✅ 延遲加載
  • ❌ 多線程下會創建多個實例(競態條件)

3. 同步方法 — 線程安全但性能差

public static synchronized Singleton getInstance() {
    if (instance == null) instance = new Singleton();
    return instance;
}
  • ✅ 線程安全
  • ❌ 每次調用都加鎖,並發性能極差(實際只需首次加鎖)

4. 雙重檢查鎖 (DCL) — 看似完美實有陷阱

// 錯誤版:缺少 volatile
public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) instance = new Singleton();
        }
    }
    return instance;
}

問題:instance = new Singleton() 非原子操作(分配內存 → 初始化對象 → 賦值引用),JVM 可能重排序為「分配 → 賦值 → 初始化」。線程 A 執行到賦值時,instance 非 null 但對象未初始化完成,線程 B 進入首次判斷見非 null 直接返回,導致使用半初始化對象。

5. 雙重檢查鎖 + volatile — 標準正確寫法 (JDK 5+)

public class Singleton {
    private static volatile Singleton instance; // 關鍵:禁止指令重排
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {                  // 第一次檢查(無鎖,性能)
            synchronized (Singleton.class) {     // 加鎖
                if (instance == null) {          // 第二次檢查(防止重複創建)
                    instance = new Singleton();  // volatile 保證初始化可見性
                }
            }
        }
        return instance;
    }
}
  • volatile 禁止指令重排序,確保「初始化完成」先於「引用賦值」對其他線程可見。
  • 首次檢查避免不必要的同步開銷,後續調用直接返回,性能優異。

其他優秀變體

靜態內部類 (推薦,JDK 1.2+)

public class Singleton {
    private Singleton() {}
    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() { return Holder.INSTANCE; }
}
  • 利用類加載機制天然線程安全。
  • 延遲加載:Holder 類僅在首次調用 getInstance() 時加載。
  • 代碼簡潔,無同步、無 volatile,最推薦。

枚舉單例 (Effective Java 作者 Josh Bloch 推薦)

public enum Singleton {
    INSTANCE;
    public void doSomething() { ... }
}
  • 天然線程安全、防反射攻擊、防序列化重複創建。
  • 語法最簡潔,但無法延遲加載(枚舉類加載即創建),且不能繼承其他類。

性能與選擇建議

寫法線程安全延遲加載性能推薦指數
餓漢式✅❌極高⭐⭐⭐ (常用、簡單場景)
靜態內部類✅✅極高⭐⭐⭐⭐⭐ (通用首選)
枚舉✅❌極高⭐⭐⭐⭐ (需防序列化/反射)
DCL + volatile✅✅高⭐⭐⭐ (教學理解、老代碼維護)
同步方法✅✅低⭐ (不推薦)

常見面試追問

  1. 為什麼 DCL 需要 volatile? 禁止指令重排序,保證對象發布時已完全初始化(happens-before)。
  2. 反射能否破壞單例? 构造器私有化可被 setAccessible(true) 繞過。枚舉單例免疫(JVM 保證)。防御:構造器中檢查 if (instance != null) throw new IllegalStateException()。
  3. 序列化/反序列化能否破壞? 實現 Serializable 且無 readResolve() 會創建新對象。防御:private Object readResolve() { return instance; }。枚舉單例免疫。
  4. 類加載器隔離下單例是否唯一? 不同類加載器加載同一類視為不同類型,各自擁有單例。Web 容器、OSGi 等模塊化環境需注意。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved