首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

單例模式在多線程環境下的並發問題與三種解決方案深度解析

     編輯:Java 程式設計入門

問題現場:為什麼連接數飆升到 100+?

某項目使用 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;
    }
}
  • 優點:實現最簡單,無鎖,性能最好,JVM 保證線程安全。
  • 缺點:無延遲加載,啟動時就佔用資源;若構造過程拋異常會導致類加載失敗;若調用 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() 非原子操作,分三步:

  1. 分配內存空間
  2. 調用構造函數初始化對象
  3. 將引用指向分配的內存

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 關鍵字
  • 最推薦的通用單例寫法

回到 Memcached 實戰修正

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() 接口,而非暴露銷毀方法。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved