單例模式保證 JVM 中僅存在一個實例,常作為配置信息載體。但在多線程環境下,實例化同步與屬性更新同步兩大問題若處理不當,會導致多實例洩露、配置讀取異常等嚴重後果。本文深度剖析問題根因,提供三種解決方案並對比優劣。
public class GlobalConfig {
private static GlobalConfig instance = null;
private Vector properties = null;
private GlobalConfig() {
// 從 DB/文件加載配置,耗時操作
}
public static GlobalConfig getInstance() {
if (instance == null) { // 線程 A 進入
instance = new GlobalConfig(); // 線程 B 也可能進入此行
}
return instance;
}
}
後果:多線程並發調用 getInstance() 時,可能創建多個實例,導致配置不一致(如部分線程讀到舊配置,部分讀到新配置)。
private static synchronized void syncInit() {
if (instance == null) {
instance = new GlobalConfig();
}
}
public static GlobalConfig getInstance() {
if (instance == null) syncInit(); // 雙重檢查減少同步範圍
return instance;
}
同步塊僅在首次初始化執行,後續訪問無鎖開銷,性能影響可接受。
配置熱更新線程修改 properties 時,業務線程可能正在讀取,導致讀到半寫入狀態或 null。
public class GlobalConfig {
private static GlobalConfig instance;
private Vector properties = null;
private boolean isUpdating = false;
private int readCount = 0;
// ... 初始化同步代碼同上 ...
public synchronized void update(String data) {
// 寫入前等待所有讀者退出
while (readCount > 0) {
try { wait(); } catch (Exception e) {}
}
isUpdating = true;
// 更新 properties...
isUpdating = false;
notifyAll();
}
public Vector getProperties() {
synchronized (this) {
readCount++;
}
try {
return properties; // 讀取過程
} finally {
synchronized (this) {
readCount--;
if (readCount == 0) notifyAll();
}
}
}
}
缺點:代碼複雜,鎖競爭激烈時性能下降,易死鎖。
public class GlobalConfig {
private static GlobalConfig instance = null;
private Vector properties = null;
private GlobalConfig() {
// 加載配置到 properties
}
// ... getInstance 同步初始化同上 ...
public void updateProperties() {
// 關鍵:創建新實例加載最新配置
GlobalConfig shadow = new GlobalConfig();
// 原子性替換引用(volatile 保證可見性)
this.properties = shadow.getProperties();
}
public Vector getProperties() {
return properties; // 無鎖讀取
}
}
優勢:
關鍵點:properties 應聲明為 volatile 保證寫入對後續讀取可見。
| 方案 | 初始化同步 | 更新同步 | 讀取性能 | 代碼複雜度 | 推薦度 |
|---|---|---|---|---|---|
| 同步方法 + 讀寫鎖 | ✅ | ✅ (鎖) | 低(需加鎖) | 高 | ⭐⭐ |
| 同步方法 + 影子實例 | ✅ | ✅ (引用替換) | 極高(無鎖) | 低 | ⭐⭐⭐⭐⭐ |
| Enum 單例 + 影子實例 | ✅ (天然) | ✅ (引用替換) | 極高 | 最低 | ⭐⭐⭐⭐⭐ |
public enum GlobalConfig {
INSTANCE;
private volatile Vector properties;
GlobalConfig() {
loadConfig();
}
private void loadConfig() {
// 從 DB/文件加載
this.properties = new Vector();
// ...
}
public Vector getProperties() {
return properties;
}
public void reload() {
GlobalConfig shadow = new GlobalConfig(); // 編譯報錯:enum 不能 new
// 正確做法:重新加載到臨時變量,再原子替換
Vector newProps = loadFromSource();
this.properties = newProps;
}
private Vector loadFromSource() { /* ... */ }
}
注意:Enum 不能 new,影子實例模式需改為「加載到臨時變量 → 原子替換字段」。
synchronized、static final、Enum 或雙重檢查鎖(需 volatile)properties 只讀,更新時整體替換volatile單例模式在多線程環境下的核心難點不在「創建唯一實例」,而在於「如何安全發布與更新狀態」。影子實例/不可變對象+原子替換模式,以空間換時間、以簡單換可靠,是解決此類問題的通用最佳模式。