某項目使用 Memcached 客戶端,採用單例模式管理連接,理論上應只建立一個連接。但監控發現連接數長期維持在 100 左右。排查代碼發現,單例的 createClient() 方法沒有線程安全保護:
// 有缺陷的單例實現
public class MemcachedClientFactory {
private static MemcachedClient memcachedClient;
private static MemcachedConnectionBuilder builder;
private static String servers;
private MemcachedClientFactory() {}
public static MemcachedClient createClient() {
if (memcachedClient == null) { // ① 多線程同時通過檢查
memcachedClient = new MemcachedClient(
builder.build(), AddrUtil.getAddresses(servers));
}
return memcachedClient;
}
}
根因:多線程並發進入 ① 處,均判斷 memcachedClient == null,導致重複創建多個客戶端實例,每個實例持有獨立連接池,最終連接數失控。
public class MemcachedClientFactory {
// 類加載時由 ClassLoader 保證線程安全,直接完成實例化
private static final MemcachedClient INSTANCE =
new MemcachedClient(builder.build(), AddrUtil.getAddresses(servers));
public static MemcachedClient createClient() {
return INSTANCE;
}
}
shutdown() 後無法重建。public static synchronized MemcachedClient createClient() {
if (memcachedClient == null) {
memcachedClient = new MemcachedClient(...);
}
return memcachedClient;
}
public class Singleton {
// volatile 禁止指令重排序,保證構造完成前其他線程不可見
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次檢查:無鎖,性能好
synchronized (Singleton.class) { // 同步塊:僅首次創建時爭搶
if (instance == null) { // 第二次檢查:防止重複創建
instance = new Singleton();
}
}
}
return instance;
}
}
volatile?instance = new Singleton() 非原子操作,分三步:
JVM 允許指令重排序(1→3→2),導致線程 B 看到 instance != null 但對象尚未初始化完成,隨後訪問字段觸發 NPE 或邏輯錯誤。volatile 禁止此類重排序,構建 Happens-Before 關係。
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // 首次調用觸發 Holder 類初始化,JVM 保證線程安全
}
}
getInstance() 時才初始化volatile、synchronized 關鍵字public class MemcachedClientFactory {
private static final class Holder {
static final MemcachedClient INSTANCE =
new MemcachedClient(builder.build(), AddrUtil.getAddresses(servers));
}
public static MemcachedClient createClient() {
return Holder.INSTANCE;
}
// 允許業務層在極端情況下重建(如網絡抖動後主動重連)
public static void forceReconnect() {
// 需配合 synchronized 或 AtomicReference 實現線程安全重建
}
}
| 方案 | 線程安全 | 延遲加載 | 性能 | 推薦場景 |
|---|---|---|---|---|
| 餓漢式 | ✅ | ❌ | 極高 | 構造輕量、必須使用、啟動即需用 |
| 同步鎖 | ✅ | ✅ | 低 | 調用頻率極低、構造開銷大 |
| 雙重檢查鎖 | ✅ (需 volatile) | ✅ | 高 | 高併發、延遲加載、JDK 5+ |
| 靜態內部類 | ✅ | ✅ | 極高 | 通用首選 |
| 枚舉單例 | ✅ | ❌ | 極高 | 防反序列化破壞、防反射攻擊 |
if (null) { synchronized { new } }),依然有重排序風險。shutdown()/close(),應由生命周期管理器(如 Spring @PreDestroy)統一控制。healthCheck() 與 rebuild() 接口,而非暴露銷毀方法。