單例模式是最基礎也最常被面試考察的設計模式。本文透過一個「日誌工具類」的演進故事,層層剖析單例實作的坑與優化:從最基本的懶漢式、線程安全問題、同步鎖效能瓶頸、雙重檢查鎖(DCL)的正確寫法,到最終推薦的餓漢式與靜態內部類寫法,助你徹底掌握生產級單例寫法。
專案中需要一個可控制列印等級的日誌工具類 LogUtil。若每次列印都 new LogUtil(),不僅浪費記憶體,且無法共享 level 設定。Leader 要求改為單例模式,確保 JVM 中僅存在一個實例。
public class LogUtil {
private static LogUtil instance;
private LogUtil() {}
public static LogUtil getInstance() {
if (instance == null) {
instance = new LogUtil(); // 多線程下可能創建多個實例
}
return instance;
}
}
缺陷: 多線程並發呼叫 getInstance() 時,可能進入 if 區塊多次,產生多個實例。
public synchronized static LogUtil getInstance() {
if (instance == null) instance = new LogUtil();
return instance;
}
缺陷: 每次呼叫都需獲取鎖,即使實例已建立,嚴重影響高併發效能。
public static LogUtil getInstance() {
if (instance == null) { // 第一次檢查,無鎖讀取
synchronized (LogUtil.class) { // 同步塊
if (instance == null) { // 第二次檢查,防止重複創建
instance = new LogUtil();
}
}
}
return instance;
}
關鍵細節:
instance 必須宣告為 volatile,防止指令重排導致其他線程看到「半初始化」的物件。if 確保只有首次建立時進入同步塊,後續呼叫直接返回,效能極佳。public class LogUtil {
private LogUtil() {}
private static class Holder {
static final LogUtil INSTANCE = new LogUtil();
}
public static LogUtil getInstance() {
return Holder.INSTANCE;
}
}
原理: 利用類別載入機制保證線程安全;Holder 類別只有在首次呼叫 getInstance() 時才會被載入初始化,實現延遲載入。
public class LogUtil {
private static final LogUtil INSTANCE = new LogUtil();
private LogUtil() {}
public static LogUtil getInstance() { return INSTANCE; }
}
優點: 無線程安全問題,程式碼最簡潔。缺點: 無延遲載入,若實例建構耗時且未被使用會浪費資源。
public final class LogUtil {
public static final int DEBUG = 0, INFO = 1, ERROR = 2, NOTHING = 3;
private int level = DEBUG;
private LogUtil() {}
private static class Holder {
static final LogUtil INSTANCE = new LogUtil();
}
public static LogUtil getInstance() {
return Holder.INSTANCE;
}
public void debug(String msg) { if (DEBUG >= level) System.out.println("[DEBUG] " + msg); }
public void info(String msg) { if (INFO >= level) System.out.println("[INFO] " + msg); }
public void error(String msg) { if (ERROR >= level) System.out.println("[ERROR] " + msg); }
// 使用範例
public static void main(String[] args) {
LogUtil.getInstance().debug("Hello Singleton!");
}
}
| 寫法 | 線程安全 | 延遲載入 | 效能 | 推薦場景 |
|---|---|---|---|---|
| 懶漢式(無鎖) | ❌ | ✅ | 高 | 單線程或教學示範 |
| 同步方法 | ✅ | ✅ | 低 | 不推薦 |
| DCL (+volatile) | ✅ | ✅ | 高 | 需手動控制初始化時機 |
| 靜態內部類 | ✅ | ✅ | 高 | 最推薦(通用) |
| 餓漢式 | ✅ | ❌ | 最高 | 實例輕量、必定會用到 |
| Enum 單例 | ✅ | ❌ | 高 | 防反序列化破壞、防反射攻擊 |
實務上,**靜態內部類**與**Enum**是最穩健的選擇;若確定實例極輕量且必用,餓漢式最簡單直接。