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

Java 線程安全單例模式五種實現方案深度對比

     編輯:Java 程式設計入門

單例模式在多執行緒環境下確保「唯一實例」與「高效能」往往互為制約。本文系統梳理五種主流線程安全單例實現,從餓漢式到雙重檢查鎖(DCL),深度解析其核心原理、優缺點、適用場景及常見錯誤寫法,助你根據業務需求做出最佳架構決策。

方案一:餓漢式 —— 最簡單、最可靠的基準方案

public class Singleton {
    // 類載入時即實例化,ClassLoader 機制天然保證線程安全
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}
  • 優點:零同步開銷、程式碼極簡、JVM 保證原子性、免疫指令重排序。
  • 缺點:無延遲載入(類載入即佔用記憶體);建構子拋異常會導致類載入失敗(ExceptionInInitializerError)。
  • 適用:物件輕量、確定會被使用、對啟動速度不敏感的場景。

方案二:靜態內部類(Holder 模式) —— 完美平衡延遲載入與效能

public class Singleton {
    private Singleton() {}

    // 只有首次呼叫 getInstance() 才會載入 SingletonHolder,進而初始化 INSTANCE
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  • 核心機制:利用 JVM 類載入器的「類初始化鎖」與「延遲載入」特性,無需額外同步關鍵字。
  • 優點:真正 Lazy Loading、首次呼叫無同步開銷、防反序列化破壞(需配合 readResolve)、被《Java Concurrency in Practice》推薦為標準寫法。
  • 缺點:無法傳遞建構參數;極少數場景下類載入時機不可控。

方案三:同步方法(懶漢式加鎖) —— 簡單但效能差

public class Singleton {
    private static Singleton instance = null;
    private Singleton() {}

    // 整個方法加鎖,每次呼叫都爭搶鎖
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 缺點:效能極差,實例建立後後續所有呼叫仍需進入同步塊,鎖爭用嚴重。
  • 教學價值:理解「檢查-建立」原子性問題的基礎,引出 DCL 優化動機。

方案四:雙重檢查鎖(DCL)v1.0 —— 經典但有 Bug 的寫法

// ❌ 錯誤示範:缺少 volatile,存在指令重排序風險
public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton(); // 非原子操作:分配記憶體 -> 建構子 -> 賦值引用
            }
        }
    }
    return instance;
}

致命缺陷:JVM 允許指令重排序(分配記憶體 → 賦值引用 → 執行建構子),導致其他執行緒看到 instance != null 但物件尚未完全初始化,存取時拋 NPE。

方案五:雙重檢查鎖(DCL)v2.0 —— 加上 volatile 的標準正確寫法

public class Singleton {
    // volatile 禁止指令重排序,保證「建構完成」先於「引用賦值」可見
    private static volatile Singleton instance = null;
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次檢查:無鎖快速路徑
            synchronized (Singleton.class) {       // 同步塊:僅首次建立爭搶
                if (instance == null) {            // 第二次檢查:防止重複建立
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 關鍵點:volatile 建立 Happens-Before 關係,確保建構子執行完成對其他執行緒可見。
  • 優點:Lazy Loading、首次建立後無鎖、記憶體佔用最小。
  • 缺點:程式碼相對複雜;Java 9+ 可考慮 VarHandle 替代;無法防反射/序列化破壞。

進階補充:防反射與序列化破壞

上述所有基於建構子的方案均可被反射(Constructor.setAccessible(true))或序列化/反序列化破壞。唯一完美免疫的是枚舉單例:

public enum Singleton {
    INSTANCE;
    public void doWork() { /* ... */ }
}

綜合對比與選型決策樹

方案線程安全延遲載入效能(穩態)防反射防序列化程式碼複雜度推薦指數
餓漢式✅❌⭐⭐⭐⭐⭐❌❌極低⭐⭐⭐
靜態內部類✅✅⭐⭐⭐⭐⭐❌需 readResolve低⭐⭐⭐⭐⭐
同步方法✅✅⭐❌❌極低⭐
DCL (volatile)✅✅⭐⭐⭐⭐❌需 readResolve中⭐⭐⭐⭐
枚舉✅❌⭐⭐⭐⭐⭐✅✅極低⭐⭐⭐⭐⭐

選型建議

  1. 通用首選:靜態內部類 —— 平衡了延遲載入、效能、簡潔性,無特殊理由首選它。
  2. 安全敏感場景:枚舉 —— 需防反射攻擊、序列化破壞,或需實作介面時。
  3. 極簡場景:餓漢式 —— 物件極輕量、確定會用、不介意啟動時初始化。
  4. 手動控制初始化時機:DCL + volatile —— 需在 getInstance() 中傳參、或維護舊專案已使用此模式。
  5. 避免使用:同步方法 —— 除非教學演示,生產環境效能不可接受。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved