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

Java 單例模式全解析:餓漢、懶漢、登記式三大實現與線程安全深度剖析

     編輯:Java 相關知識

單例模式確保一個類在全局僅有一個實例,並提供全局訪問點。它是資源管理(連接池、配置管理器、日誌記錄器等)的首選模式。本文系統剖析 Java 中單例的三大經典實現——餓漢式、懶漢式、登記式,深度解析雙重檢查鎖(DCL)的線程安全機制、類載入器陷阱、垃圾回收陷阱,並提供生產級配置管理器實戰案例。

一、單例模式核心特性

  1. 全局唯一實例
  2. 自我實例化(私有建構子)
  3. 提供全局靜態訪問方法 getInstance()
  4. 理論上可推廣至有限多例(如連接池)

二、三大經典實現對比

1. 餓漢式——最簡單、線程安全、類載入即實例化


public class EagerSingleton {
    private EagerSingleton() {}  // 私有建構子
    
    private static final EagerSingleton INSTANCE = new EagerSingleton();
    
    public static EagerSingleton getInstance() {
        return INSTANCE;
    }
}

UML:餓漢式單例類圖

特點:

  • ✅ 類載入機制天然保證線程安全(JVM 類初始化鎖)
  • ✅ 實現最簡潔,無同步開銷
  • ❌ 類載入即實例化,即使未使用也佔用資源
  • ❌ 無法繼承(私有建構子)
  • ❌ 無法處理實例化過程中的異常

2. 懶漢式——首次使用時實例化,雙重檢查鎖保證線程安全


public class LazySingleton {
    private LazySingleton() {}
    
    private static volatile LazySingleton instance = null;  // 關鍵:volatile
    
    public static LazySingleton getInstance() {
        if (instance == null) {                    // 第一次檢查:無鎖,效能最佳
            synchronized (LazySingleton.class) {   // 類級別鎖
                if (instance == null) {            // 第二次檢查:防止重複建立
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

UML:懶漢式單例類圖

關鍵點深度解析:

元素作用缺失後果
volatile禁止指令重排序,保證構造完成前不對其他線程可見其他線程可能看到「半初始化」物件
第一次 if (instance == null)避免不必要的同步開銷每次調用都進入同步塊,效能大幅下降
第二次 if (instance == null)防止多線程同時通過第一次檢查後重複建立建立多個實例,違反單例語義
synchronized (LazySingleton.class)類級別鎖,保護關鍵區無法保證原子性

⚠️ 歷史遺留問題:JDK 1.4 及之前版本的記憶體模型有缺陷,DCL 可能失效。JDK 5+ 引入 volatile 語義修復後,DCL 才真正線程安全。

3. 登記式——支援繼承,解決餓漢/懶漢無法繼承的痛點


public class RegSingleton {
    protected RegSingleton() {}  // protected 允許子類建構
    
    private static final Map REGISTRY = new HashMap<>();
    
    static {
        // 自我註冊
        RegSingleton singleton = new RegSingleton();
        REGISTRY.put(singleton.getClass().getName(), singleton);
    }
    
    public static synchronized RegSingleton getInstance(String name) {
        if (name == null) name = RegSingleton.class.getName();
        if (!REGISTRY.containsKey(name)) {
            try {
                REGISTRY.put(name, Class.forName(name).newInstance());
            } catch (Exception e) { /* 處理異常 */ }
        }
        return REGISTRY.get(name);
    }
}

// 子類實現
public class RegSingletonChild extends RegSingleton {
    public static RegSingletonChild getInstance() {
        return (RegSingletonChild) RegSingleton.getInstance(RegSingletonChild.class.getName());
    }
}

UML:登記式單例類圖

優缺點權衡:

  • ✅ 支援繼承體系
  • ✅ 運行時動態決定實例類型
  • ❌ 父類實例必須先建立(潛在浪費)
  • ❌ 子類建構子必須 public,繞過註冊表可直接 new 破壞單例
  • ❌ 需自行處理多線程同步(註冊表操作)

三、Java 類垃圾回收陷阱與 ObjectKeeper 解決方案

問題背景

  • JDK 1.1:激進的類卸載,甚至系統類可能被回收 → 單例類意外消失
  • JDK 1.2+:收緊規則,啟動/系統類載入器載入的類永不回收;自定義載入器載入的類僅在載入器回收時才回收

ObjectKeeper 看守者模式


public class ObjectKeeper extends Thread {
    private static final ObjectKeeper KEEPER = new ObjectKeeper();
    private static final Vector KEPT_OBJECTS = new Vector<>();
    
    private ObjectKeeper() { new Thread(this).start(); }
    
    public void run() {
        try { join(); } catch (InterruptedException ignored) {}
    }
    
    public static void keepObject(Object obj) {
        KEPT_OBJECTS.add(obj);  // 強引用持有,阻止 GC
    }
    
    public static void discardObject(Object obj) {
        KEPT_OBJECTS.remove(obj); // 釋放引用,允許 GC
    }
}

UML:ObjectKeeper 類圖

使用方式:


ObjectKeeper.keepObject(ConfigManager.getInstance());
// ... 系統運行 ...
ObjectKeeper.discardObject(ConfigManager.getInstance());

四、實戰案例:配置管理器 ConfigManager

需求:單例管理 singleton.properties,支援運行時檢測檔案修改並熱重載。

完整實現


public class ConfigManager {
    private static final String PFILE = System.getProperty("user.dir") + "/Singleton.properties";
    private static final ConfigManager INSTANCE = new ConfigManager(); // 餓漢式
    
    private File file;
    private long lastModified;
    private Properties props;
    
    private ConfigManager() {
        file = new File(PFILE);
        lastModified = file.lastModified();
        reload();
    }
    
    public static ConfigManager getInstance() {
        return INSTANCE;
    }
    
    public Object getConfigItem(String name, Object defaultVal) {
        long currentModified = file.lastModified();
        
        // 檢測檔案變更
        if (currentModified > lastModified) {
            reload();
            lastModified = currentModified;
        } else if (currentModified == 0) {
            // 檔案被刪除
            return defaultVal;
        }
        
        Object val = props.getProperty(name);
        return val != null ? val : defaultVal;
    }
    
    private void reload() {
        props = new Properties();
        try (FileInputStream fis = new FileInputStream(file)) {
            props.load(fis);
        } catch (IOException e) { /* 記錄日誌 */ }
    }
}

properties 檔案範例


node1.item1=How
node1.item2=are
node2.item1=you
node2.item2=doing
node3.item1=?

五、常見問題答疑

  1. Q: 為何不用 static 變量而要建類?
    A: 變量無法自我初始化、無繼承關係、初始化位置難控制。類封裝了「如何建立、何時建立、建立幾個」的完整邏輯。
  2. Q: synchronized 加在方法上 vs 類鎖塊的差異?
    A: 方法鎖 = 每次調用都同步(愚蠢);類鎖塊 + DCL = 僅首次同步(高效)。
  3. Q: DCL 兩層 if 可否省略?
    A: 內層不可省(防重複建立),外層可省但效能大幅下降(每次同步)。
  4. Q: m_keptObjects 用 Vector 還是 HashMap?
    A: HashMap 更省資源(僅存引用,無同步開銷),但需外部同步。

六、單例驗證技巧

通過以下指標驗證單例唯一性:

  • 實例建立時間戳
  • System.identityHashCode(instance)
  • 類載入器 instance.getClass().getClassLoader()
  • 實例總數計數器
  • 實例化序數

七、總結與選型建議

場景推薦實現理由
一般業務單例、資源管理器餓漢式簡單、安全、JVM 保證線程安全、無同步開銷
實例化極其耗時/資源、且可能不被使用懶漢式 (DCL + volatile)延遲初始化,節省啟動資源
需支援單例繼承體系、插件架構登記式允許子類擴展,運行時動態註冊
JDK 1.1 環境、或自定義類載入器環境餓漢式 + ObjectKeeper防止類被意外卸載

現代最佳實踐:優先考慮 枚舉單例(Effective Java Item 3)——天然線程安全、防序列化破壞、防反射攻擊、代碼最簡潔:


public enum EnumSingleton {
    INSTANCE;
    public void doSomething() { ... }
}
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved