在軟件設計中,有時我們希望某個類的實例始終是唯一的,即無論在何處訪問這個類,都能夠得到同一個實例。單例模式(Singleton Pattern)就是為了解決這個問題而產生的。單例模式確保一個類只有一個實例,并提供一個全局訪問點。
1.定義
單例模式是一種創建型設計模式,確保一個類只有一個實例,并提供一個全局訪問點來訪問這個實例。其主要思想是將類的構造函數私有化,并通過一個靜態方法來控制實例的創建和訪問。
2.常見實現方式
單例模式有多種實現方式,下面介紹幾種常見的實現方式:
2.1餓漢式(Eager Initialization)
餓漢式是在類加載時就創建實例,這樣可以確保線程安全,并且在類首次使用前完成實例化。
示例代碼:
public class Singleton {private static final Singleton INSTANCE = new Singleton();private Singleton() {// 私有構造函數,防止外部實例化}public static Singleton getInstance() {return INSTANCE;}
}
優點:簡單,易于理解,線程安全
缺點:類加載時即創建實例,可能造成資源浪費
如果你一定會使用該類,這種方式無疑是最簡單的方法
2.2 懶漢式(Lazy Initialization)
懶漢式是在第一次調用 getInstance() 方法時創建實例。這種方式避免了餓漢式的資源浪費問題。
示例代碼:
public class Singleton {private static Singleton instance;private Singleton() {// 私有構造函數,防止外部實例化}public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}
優點:實現簡單,延遲實例化,避免資源浪費
缺點:使用了 synchronized,在高并發情況下性能可能較差
每次調用getInstance()
都會進行同步檢查,這樣會消耗不必要的資源,不推薦使用
2.3雙重檢查鎖(Double-Checked Locking)
雙重檢查鎖在懶漢式的基礎上,通過減少使用 synchronized 來提高性能。
示例代碼:
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; // 返回實例}
}
優點:延遲實例化,提高了性能
缺點:實現復雜,容易出錯
同步代碼塊含義:因為可能會有多個線程同時通過了第一次檢查,在進入同步塊之后,再次檢查可以確保只有一個線程創建實例,最大限度地在提升性能的條件下保證了線程安全。
emm… 個人不喜歡這種笨重寫法
2.4 靜態內部類(Static Inner Class)
這種方式使用了類加載機制來確保線程安全,同時實現了延遲加載。
示例代碼:
public class Singleton {private Singleton() {// 私有構造函數,防止外部實例化}// 靜態內部類,利用類加載機制保證線程安全且延遲加載private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance() {return SingletonHolder.INSTANCE;}
}
優點:延遲實例化,線程安全,實現簡單
缺點:無法傳遞外部參數
這時候就會有同學要問了,何為類加載機制,問的好,所謂類加載機制就算:JVM 在加載類的過程中,靜態內部類 SingletonHolder 中的靜態變量 INSTANCE 只會被實例化一次,由JVM保證其線程安全性,所以在多線程環境下可以安全地使用。
2.5 枚舉(Enum)
這種方法是Effective Java作者Joshua Bloch推薦的單例實現方式之一,它解決了傳統單例模式實現中的一些問題,比如序列化、反射攻擊等。
示例代碼:
public enum Singleton {INSTANCE;public void doSomething() {// 業務方法}
}
優點:簡潔,線程安全,防止反序列化破壞單例
缺點:無法靈活控制實例化過程
使用方法:
Singleton.INSTANCE.doSomething();
特點和優勢
-
線程安全性:
枚舉類型的實例創建是線程安全的,JVM在加載枚舉類型時會通過類加載器保證只實例化一次。因此,多線程環境下也能保證單例的唯一性。 -
防止反射攻擊:
枚舉類型的實例創建是由JVM控制的,因此無法通過反射來創建枚舉類的實例。這樣可以防止反射攻擊,即使是在枚舉類中添加了私有構造函數也不例外。 -
防止序列化問題:
Java枚舉類型在序列化和反序列化時會自動處理,確保在序列化和反序列化過程中都是單例的。 -
簡潔且高效:
枚舉實現單例模式非常簡潔,只需聲明一個枚舉類型即可,不需要額外的代碼來保證線程安全和單例特性。
3.單例模式的注意事項
- 線程安全:確保在多線程環境下一個類只有一個實例。
- 延遲加載:盡量避免在類加載時就實例化,除非明確知道實例一定會被使用。
- 防止反射攻擊:通過在構造函數中添加判斷來防止反射創建多個實例。
- 防止反序列化破壞單例:在實現
Serializable
接口時,提供 `readResolve 方法。
4.總結
五種創建單例的方式,大家按需選擇,核心思想都是確保一個類只有一個實例,并提供全局訪問點,沒有最好的,只有最適合的,理解不同實現方式的優缺點,可以幫助我們在實際開發中選擇最合適的方案。