單例模式看似簡單,實則暗藏多線程、指令重排、類加載器、序列化、反射等陷阱。本文帶你經歷七個版本迭代,從 naïve 實現到枚舉單例,構建生產級認知。
| 版本 | 核心改進 | 剩餘問題 |
|---|---|---|
| 1.0 懶漢式 | 私有構造、靜態工廠、實例緩存 | 多線程不安全 |
| 1.1 同步方法 | 加鎖 synchronized | 鎖粒度過大,性能差 |
| 1.2 雙重檢查雛形 | 鎖內再次判空 | 指令重排導致半初始化對象洩漏 |
| 1.3 雙重檢查鎖定 | 外層判空避免重複加鎖 | 仍有指令重排問題 |
| 1.4 volatile 修復 | 禁止重排,JDK 1.5+ 有效 | 依賴 JDK 版本,代碼較繁瑣 |
| 1.5 餓漢式 | 類加載時初始化,天然線程安全 | 無法延遲加載,構造依賴外部資源時風險大 |
| 1.6 靜態內部類 | 利用類加載機制,延遲+線程安全+無鎖 | 完美解決大多數場景 |
| 1.7 枚舉單例 | JVM 保證單一實例,防反射/序列化破壞 | 推薦:代碼最簡、功能最全 |
instance = new Singleton() 非原子操作:分配內存 → 初始化對象 → 引用賦值。JIT 可能重排為 1-3-2,導致其他線程見到非 null 但未初始化完成的對象。volatile 插入內存屏障禁止重排。
不同 ClassLoader 加載同一單例類會產生多個 Class 對象,各自持有獨立靜態字段。解決:父子委派模型下由公共祖先加載單例類,或使用模塊化系統(JPMS)控制可見性。
readResolve() 返回現有實例。if (instance != null) throw Exception 防御;枚舉天然免疫。單進程單例無法跨進程。分布式場景需用分布式鎖、ZooKeeper、Redis SETNX 等協調機制實現「全局單例」。
public class SingletonFactory {
private static final Map<Class<?>, Object> INSTANCES = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked")
public static <T> T getInstance(Class<T> clazz) {
return (T) INSTANCES.computeIfAbsent(clazz, c -> {
try { return c.getDeclaredConstructor().newInstance(); }
catch (Exception e) { throw new RuntimeException(e); }
});
}
}
單例模式演進史是 Java 并發、內存模型、類加載機制的縮影。日常開發首選枚舉單例;框架級代碼考慮靜態內部類;特殊場景(依賴注入容器、分布式)另行設計。切記:無論代碼多完美,只能在特定假設下工作,超出假設必有 Bug。