單例模式確保一個類只有一個實例,并提供全局訪問點,實現方式包括餓漢式線程安全但浪費內存;懶漢式延遲加載但需加鎖;雙重檢查鎖減少同步開銷;靜態內部類結合延遲加載和線程安全;枚舉最簡潔且防反射攻擊。應用場景如線程池、配置管理器、數據庫連接池和日志記錄器等。為防反射破壞,可在構造函數中判斷實例是否存在并拋異常,而枚舉天然防止反射攻擊。與靜態類相比,單例支持繼承、多態和延遲加載,適用需要全局實例的場景。
單例模式,簡單來說,就是確保一個類只有一個實例,并提供一個全局訪問點。實現方式多種多樣,各有千秋,選擇哪種,得看具體應用場景。
解決方案
單例模式的核心在于控制實例的創建,防止外部隨意new對象。常見的實現方式包括:
立即學習“Java免費學習筆記(深入)”;
-
餓漢式(Eager Initialization):
直接在類加載的時候就創建實例。線程安全,簡單粗暴,但缺點是如果這個單例一直沒被用到,就浪費了內存。
-
懶漢式(Lazy Initialization):
在第一次使用的時候才創建實例。優點是延遲加載,節省內存。但線程不安全,需要加鎖。
public class SingletonLazy { private static SingletonLazy instance; private SingletonLazy() {} public static synchronized SingletonLazy getInstance() { if (instance == null) { instance = new SingletonLazy(); } return instance; } }
加 synchronized 關鍵字保證了線程安全,但每次獲取實例都要同步,效率較低。
-
雙重檢查鎖(double-Checked Locking):
在懶漢式的基礎上,通過雙重檢查和 volatile 關鍵字來提高效率。
public class SingletonDoubleCheck { private volatile static SingletonDoubleCheck instance; private SingletonDoubleCheck() {} public static SingletonDoubleCheck getInstance() { if (instance == null) { synchronized (SingletonDoubleCheck.class) { if (instance == null) { instance = new SingletonDoubleCheck(); } } } return instance; } }
volatile 關鍵字防止指令重排序,確保多線程環境下instance的正確初始化。雙重檢查減少了同步的開銷,只有在第一次創建實例的時候才需要同步。不過,早期的jvm版本中,volatile 可能存在問題,導致DCL失效。
-
靜態內部類(Static Inner Class):
利用類加載機制保證線程安全,同時實現延遲加載。
public class SingletonStaticInner { private SingletonStaticInner() {} private static class SingletonHolder { private static final SingletonStaticInner instance = new SingletonStaticInner(); } public static SingletonStaticInner getInstance() { return SingletonHolder.instance; } }
當外部類 SingletonStaticInner 被加載時,靜態內部類 SingletonHolder 并不會被加載,只有當調用 getInstance() 方法時,才會加載 SingletonHolder,從而創建單例實例。這種方式既保證了線程安全,又實現了延遲加載,推薦使用。
-
枚舉(enum):
最簡潔的單例實現方式,線程安全,防止反射攻擊和序列化攻擊。
public enum SingletonEnum { INSTANCE; public void doSomething() { // ... } }
枚舉單例是Effective Java作者極力推薦的,它利用JVM保證線程安全和唯一性。
單例模式在多線程環境下如何保證線程安全?
保證線程安全的關鍵在于防止多個線程同時創建實例。餓漢式因為在類加載時就創建了實例,所以天生線程安全。懶漢式需要加鎖,或者使用雙重檢查鎖和 volatile 關鍵字。靜態內部類和枚舉則利用了類加載機制,由JVM保證線程安全。
單例模式有哪些應用場景?
單例模式的應用場景非常廣泛。比如:
- 線程池: 確保只有一個線程池實例來管理線程資源。
- 配置管理器: 只有一個配置管理器實例來讀取和管理配置信息。
- 數據庫連接池: 只有一個數據庫連接池實例來管理數據庫連接。
- 日志記錄器: 只有一個日志記錄器實例來記錄日志信息。
總之,任何只需要一個全局實例的場景,都可以考慮使用單例模式。
如何防止單例模式被反射破壞?
反射可以繞過私有構造函數,創建多個實例。為了防止反射攻擊,可以在構造函數中進行判斷,如果已經存在實例,則拋出異常。
public class SingletonDoubleCheck { private volatile static SingletonDoubleCheck instance; private SingletonDoubleCheck() { if (instance != null) { throw new IllegalStateException("Singleton instance already exists."); } } public static SingletonDoubleCheck getInstance() { if (instance == null) { synchronized (SingletonDoubleCheck.class) { if (instance == null) { instance = new SingletonDoubleCheck(); } } } return instance; } }
枚舉單例則天然防止反射攻擊,因為JVM會阻止通過反射創建枚舉實例。
單例模式和靜態類的區別?
雖然單例模式和靜態類都可以實現全局訪問,但它們有本質的區別。單例模式是一個類的實例,可以繼承接口和抽象類,可以被多態使用。而靜態類只是一個類的集合,不能被繼承和多態使用。此外,單例模式可以延遲加載,而靜態類在類加載時就被初始化。
在選擇單例模式還是靜態類時,要根據具體的需求來決定。如果需要繼承、多態或者延遲加載,則應該選擇單例模式。如果只是需要一個工具類,則可以選擇靜態類。