單例模式確保類別在 JVM 中僅有一個實例,並提供全域存取點。常見於執行緒池、快取、日誌、資料庫連線池等資源管理場景。本文系統性解析三種經典實作,並深入探討執行緒安全與效能權衡。
new 實例getInstance():提供全域存取public class Singleton {
private static Singleton uniqueInstance = null;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton(); // 多執行緒下可能建立多個實例
}
return uniqueInstance;
}
}
缺點:並發環境下 uniqueInstance == null 判斷與賦值非原子操作,會產生多個實例。
public class Singleton1 {
private static final Singleton1 INSTANCE = new Singleton1();
private Singleton1() {}
public static Singleton1 getInstance() { return INSTANCE; }
}
優點:JVM 類別載入機制保證原子性,無同步開銷,效能最佳。
缺點:若實例建構耗時/資源大且未被使用,造成浪費;無法處理建構過程中的異常。
public class Singleton2 {
private Singleton2() {}
private static class Holder {
static final Singleton2 INSTANCE = new Singleton2();
}
public static Singleton2 getInstance() { return Holder.INSTANCE; }
}
原理:外部類別載入時不會初始化內部類,僅在首次呼叫 getInstance() 存取 Holder.INSTANCE 時觸發初始化,JVM 保證執行緒安全。推薦為預設選擇。
public class Singleton3 {
private volatile static Singleton3 uniqueInstance; // 關鍵:禁止指令重排
private Singleton3() {}
public static Singleton3 getInstance() {
if (uniqueInstance == null) { // 第一次檢查:無鎖讀取
synchronized (Singleton3.class) { // 鎖定類別物件
if (uniqueInstance == null) { // 第二次檢查:鎖內確認
uniqueInstance = new Singleton3();
}
}
}
return uniqueInstance;
}
}
關鍵點:volatile 禁止「分配記憶體 → 建構物件 → 賦值引用」的指令重排,避免其他執行緒看到半初始化的物件。
適用場景:建構極其耗時、且極少呼叫 getInstance()、需極致延遲初始化。
public enum Singleton4 {
INSTANCE;
public void doSomething() { /* ... */ }
}
優點:語法簡潔、免費提供序列化機制、絕對防止反射攻擊與多次初始化。最推薦(除非需延遲載入或繼承類別)。
| 實作 | 執行緒安全 | 延遲載入 | 效能 | 備註 |
|---|---|---|---|---|
| 懶漢式 (無同步) | ❌ | ✅ | 最高 | 不可用於生產環境 |
| 懶漢式 (同步方法) | ✅ | ✅ | 低 | 每次呼叫皆同步,開銷大 |
| 餓漢式 | ✅ | ❌ | 最高 | 類別載入即初始化 |
| 靜態內部類 | ✅ | ✅ | 高 | 推薦 |
| DCL + volatile | ✅ | ✅ | 高 | 程式碼較複雜 |
| Enum | ✅ | ❌ | 最高 | 最推薦 (無需延遲時) |
Constructor.setAccessible(true) 強制建構。可在建構子檢查 if (INSTANCE != null) throw new IllegalStateException() 防禦。Serializable 時需加 readResolve() 回傳 INSTANCE,否則反序列化會產生新物件。選型建議:
✅ 預設用Enum(最簡單、最安全)
✅ 需延遲載入用靜態內部類
⚠️ 極特殊效能需求才考慮DCL
❌ 避免使用同步方法版懶漢式