單例模式看似簡單,實則暗藏多線程並發陷阱。本文層層遞進分析五個版本:基本懶漢、同步方法、同步塊、雙重檢查鎖定 (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 + 兩次同步塊 | 修復重排問題,效能與安全兼得 |
new Singleton() 非原子操作,JIT 編譯器可能重排為:
mem = allocate(); // 1. 分配記憶體
instance = mem; // 2. 引用賦值(此時 instance != null)
ctorSingleton(instance); // 3. 執行構造函數初始化
線程 A 執行到步驟 2 被搶佔,線程 B 見 instance != null 直接返回,**拿到未初始化完成的物件** → NPE 或數據錯誤。
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() 的「寫入引用」發生在「構造函數完成」之後,對其他線程可見。
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
public enum Singleton {
INSTANCE;
public void doSomething() {}
}
volatile**。