單例模式:確保唯一實例的設計模式
一、模式核心:保證類僅有一個實例并提供全局訪問點
在軟件開發中,有些類需要確保只有一個實例(如系統配置類、日志管理器),避免因多個實例導致狀態混亂或資源浪費。
單例模式(Singleton Pattern) 通過私有化構造方法、持有唯一實例引用、提供靜態訪問接口,確保一個類在全局范圍內只有一個實例,并提供統一的訪問入口。核心解決:
- 實例唯一性:避免創建多個實例消耗資源(如數據庫連接池、線程池)。
- 全局可訪問性:為全局提供一個訪問點,簡化客戶端調用。
- 延遲初始化:支持實例的延遲加載(按需創建),提升系統性能。
核心思想與 UML 類圖
單例模式的核心是私有化構造方法,并通過靜態方法返回唯一實例。常見實現方式包括:
- 餓漢式(類加載時立即創建實例)
- 懶漢式(第一次調用時創建實例,需處理線程安全)
二、核心實現:三種經典單例模式
1. 餓漢式單例(線程安全,類加載時創建實例)
public class EagerSingleton { // 類加載時立即創建實例(靜態變量初始化) private static final EagerSingleton instance = new EagerSingleton(); // 私有化構造方法,防止外部實例化 private EagerSingleton() { System.out.println("創建餓漢式單例實例"); } // 公共訪問方法,直接返回實例 public static EagerSingleton getInstance() { return instance; }
}
特點:
- 優點:簡單可靠,類加載時完成初始化,天然線程安全。
- 缺點:無論是否使用都會創建實例,可能浪費內存(適用于實例創建成本低的場景)。
2. 懶漢式單例(線程不安全,延遲創建實例)
public class LazySingleton { private static LazySingleton instance; private LazySingleton() { System.out.println("創建懶漢式單例實例"); } // 未加鎖,多線程環境可能創建多個實例 public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; }
}
特點:
- 優點:延遲加載,節省內存。
- 缺點:多線程環境下不安全,可能出現多個實例(需改進為線程安全版本)。
3. 線程安全的懶漢式(雙重檢查鎖定,DCL)
public class ThreadSafeSingleton { private static volatile ThreadSafeSingleton instance; // volatile 禁止指令重排 private ThreadSafeSingleton() { System.out.println("創建線程安全懶漢式單例實例"); } public static ThreadSafeSingleton getInstance() { // 第一次檢查:實例是否已創建 if (instance == null) { synchronized (ThreadSafeSingleton.class) { // 同步塊,保證線程安全 // 第二次檢查:防止多個線程同時通過第一次檢查 if (instance == null) { instance = new ThreadSafeSingleton(); } } } return instance; }
}
關鍵細節:
volatile
關鍵字:確保instance
的可見性和禁止指令重排,避免初始化未完成時被其他線程訪問。- 雙重檢查(Double-Check Locking):減少同步塊的競爭,提升性能。
三、進階:使用枚舉實現單例(推薦方式)
Java 枚舉天然支持單例模式,且簡潔可靠,自動處理序列化和反射攻擊問題。
public enum EnumSingleton { INSTANCE; // 唯一實例 // 附加方法示例 public void doSomething() { System.out.println("枚舉單例執行操作"); }
}
調用方式:
EnumSingleton.INSTANCE.doSomething(); // 直接通過枚舉成員訪問
優點:
- 簡潔高效,無需手動處理線程安全和序列化問題。
- 防止通過反射創建新實例(
Enum
類禁止反射攻擊)。
四、框架與源碼中的單例實踐
1. Spring 框架中的單例 Bean
Spring 默認創建的 Bean 是單例的,通過 BeanFactory
管理實例的唯一性。
@Service
public class UserService { // Spring 自動創建單例實例
}
2. Log4j 日志管理器
Log4j 的 Logger
類使用單例模式,確保每個類對應的日志記錄器唯一。
public class App { private static final Logger logger = Logger.getLogger(App.class); public static void main(String[] args) { logger.info("單例日志記錄器"); }
}
五、避坑指南:正確使用單例模式的 4 個要點
1. 處理序列化與反序列化攻擊
若單例類實現了 Serializable
接口,需添加 readResolve()
方法防止反序列化創建新實例:
protected Object readResolve() { return instance; // 返回現有實例,避免創建新對象
}
2. 防止反射攻擊
通過在構造方法中添加校驗,禁止通過反射創建多個實例:
private Singleton() { if (instance != null) { throw new IllegalStateException("單例實例已存在"); } // 初始化邏輯
}
3. 避免單例持有長生命周期對象
單例若持有大對象或上下文(如 ApplicationContext
),可能導致內存泄漏,需及時釋放資源。
4. 謹慎使用延遲加載
懶漢式單例需確保線程安全,否則可能引發 bug;若實例創建成本低,優先使用餓漢式或枚舉式。
六、總結:何時該用單例模式?
適用場景 | 核心特征 | 典型案例 |
---|---|---|
全局唯一配置 | 配置信息需要全局共享且唯一 | 系統配置類(ConfigManager) |
資源池管理 | 控制資源(如數據庫連接)的創建數量 | 數據庫連接池、線程池 |
日志記錄器 | 全局共享日志實例 | Log4j、Logback |
避免重復初始化 | 初始化成本高,需保證僅執行一次 | 重量級對象(如緩存管理器) |
單例模式通過嚴格控制實例數量,實現了全局狀態的統一管理。下一篇我們將探討建造者模式,解析如何分步構建復雜對象,敬請期待!
擴展思考:單例模式的缺點
- 測試困難:單例與測試框架(如 JUnit)的依賴注入沖突,需通過模擬或反射繞過。
- 違背單一職責原則:單例可能承擔業務邏輯與實例管理雙重職責,建議將實例管理抽象為獨立工廠。