今天介紹一下 單例模式(Singleton)
應用場景:配置管理類、數據庫連接池、線程池
實現方式:雙重檢查鎖定、靜態內部類、枚舉
public class ConfigManager {private static volatile ConfigManager instance;private ConfigManager() {}public static ConfigManager getInstance() {if (instance == null) {synchronized (ConfigManager.class) {if (instance == null) {instance = new ConfigManager();}}}return instance;}
}
在雙重檢查鎖定(Double-Checked Locking)的單例模式實現中,如果缺少volatile
關鍵字,可能會導致線程安全問題,具體表現為可能獲取到未完全初始化的單例對象。這是由Java內存模型(JMM)的特性決定的。
沒有volatile
時的問題
-
指令重排序問題:
- 對象初始化
instance = new ConfigManager()
不是一個原子操作,它包含三個步驟:- 分配內存空間
- 初始化對象
- 將引用指向內存地址
- 編譯器/處理器可能會進行指令重排序,導致步驟2和步驟3順序顛倒
- 這樣其他線程可能看到一個不為null但未完全初始化的對象
- 對象初始化
-
可見性問題:
- 沒有volatile修飾,一個線程對instance的修改可能不會立即對其他線程可見
- 可能導致多個線程都認為instance為null,進而創建多個實例
具體場景分析
假設沒有volatile:
// 線程A第一次調用getInstance()
if (instance == null) { // 第一次檢查synchronized (ConfigManager.class) {if (instance == null) { // 第二次檢查instance = new ConfigManager(); // 可能發生重排序}}
}
// 線程B此時調用getInstance()
// 可能看到instance不為null,但對象尚未完全初始化
為什么volatile能解決這個問題
-
禁止指令重排序:
- volatile通過內存屏障(Memory Barrier)禁止JVM和處理器對相關指令進行重排序
- 確保對象的初始化在引用賦值之前完成
-
保證可見性:
- 對volatile變量的寫操作會立即刷新到主內存
- 對volatile變量的讀操作會從主內存讀取最新值
其他解決方案
-
靜態內部類方式(推薦):
public class ConfigManager {private ConfigManager() {}private static class Holder {static final ConfigManager INSTANCE = new ConfigManager();}public static ConfigManager getInstance() {return Holder.INSTANCE;} }
- 利用類加載機制保證線程安全
- 延遲加載(Lazy Initialization)
-
枚舉方式(最安全):
public enum ConfigManager {INSTANCE;// 其他方法 }
- 防止反射攻擊
- 自動處理序列化問題
在實際項目中,靜態內部類方式是最常用的單例實現方式,既保證了線程安全又實現了延遲加載。