單例模式看似簡單,卻是面試高頻考點與生產環境 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,天然具備**延遲載入**與**線程安全**。
public enum Singleton {
INSTANCE;
public void doSomething() { System.out.println("Enum singleton working..."); }
}
核心優勢:
Constructor.newInstance() 會拋異常readResolvepublic 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()
volatile 在位,補上 readResolve單例模式的本質是「控制實例化數量 = 1」。枚舉以語言級保證最徹底解決此問題,應成為現代 Java 開發的預設選擇。下次面試或 Code Review 遇到單例,請直接問:「為何不用 enum?」