目錄
單例模式介紹
單例模式(Singleton Pattern)是 Java 中最簡單、最常用的設計模式之一。單例模式提供了一種在多線程環境下保證實例唯一性的解決方案。即:對象一經初始化,後需可以直接訪問,不需要再次實例化該類的對象。屬於設計模式三大類中的創建型模式。在Java中,一般常用在工具類的實現、對象創建開銷較大的情景下。
單例模式具有典型的三個特點:
- 單例類只能有一個實例。
- 單例類必須自己創建自己的唯一實例。(自我實例化)
- 單例類必須給所有其他對象提供這一實例。(提供全局訪問點)
單例模式雖然比較簡單,但實現方式卻多種多樣,本篇列舉了7種實現方式,並從線程安全
、高性能
、懶加載
三個維度對其進行評估,比較其優劣。
一、餓漢式
餓漢模式,顧名思義,就是採用靜態初始化的方式在類被加載時就將自身實例化,所以被形象地稱之爲餓漢式單例模式。
// final不允許被繼承
public final class HungrySingleton {
//類的成員變量(一般類都有成員變量)
private byte[] data = new byte[1024];
//第一步:私有化構造器,不允許外部new操作
private HungrySingleton(){
}
//第二步:在類初始化時立即實例化該對象,從而保證線程安全
private static HungrySingleton instance = new HungrySingleton();
//第三步:提供一個獲取全局訪問點的方法
public static HungrySingleton getInstance() {
return instance;
}
//other methods
}
餓漢式把instance
作爲"類變量"並且直接初始化,當主動使用Singleton 類時會完成instance
的創建,包括其中的實例變量都會得到初始化,比如上例中的data數組將被創建並佔用1K的空間。如果instance
被ClassLoader加載後很長一段時間才被使用,那就意味着instance
實例所開闢的堆內存會駐留更久的時間,如果一個類的成員佔用的內存資源較多,那麼採用餓漢式就有些不妥。
總結
- 線程安全。
- getInstance方法的性能比較高。
- 無法進行懶加載。
注意:上面代碼中有使用final關鍵字,強制該類不允許被繼承。若父類所有的構造器都是私有的(private修飾),那麼JVM規定該父類不允許被繼承,因爲子類的構造器都必須顯示或隱式調用父類的構造器。此時可以不使用final關鍵字聲明。
JDK餓漢式單例舉例:
二、懶漢式
懶漢式就是在第一次使用類實例的時候再去創建,和餓漢式在類初始化時就提前創建實例不同,所以就被稱爲懶漢式單例模式。
//final不允許被繼承
public final class LazySingleton {
//類的成員變量(一般類都有成員變量)
private byte[] data = new byte[1024];
// 未實例化的類變量
private static LazySingleton instance = null;
// 私有化構造器
private LazySingleton() {
}
// 運行時加載對象
public static LazySingleton getInstance() {
//判斷是否已經初始化過(沒有同步機制控制,多線程不安全)
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
//other methods
}
總結
- 線程不安全。instance是共享資源,當多個線程對其訪問時需要保證共享資源的同步性,因此線程不安全,無法保證單例的唯一性。(更加具體的原因不攤開說明了)
- 性能和懶加載,就不討論了,因爲這種方法本身就不正確。
三、懶漢式+synchronized同步
上述的懶漢式保證了實例的懶加載,但無法保證實例的唯一性,需要增加對共享資源instance的同步訪問機制,可以採用synchronized關鍵字實現。
//final不允許被繼承
public final class LazySingleton {
//類的成員變量(一般類都有成員變量)
private byte[] data = new byte[1024];
// 未實例化的類變量
private static LazySingleton instance = null;
// 私有化構造器
private LazySingleton() {
}
// 運行時加載對象(增加了synchronized,每次只能有一個線程能夠進入)
public static synchronized LazySingleton getInstance() {
//判斷是否已經初始化過(沒有同步機制控制,多線程不安全)
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
//other methods
}
總結
- 線程安全,能夠保證實例的唯一性;
- getInstance方法採用synchronized關鍵字所以性能較低
- 懶加載。
四、Double-Check式(注意有坑)
Double-Check的方式是一種更加高效的數據同步策略,只有首次初始化時才加鎖,之後多個線程獲取實例時都無需同步控制。
// final不允許被繼承
public final class LazySingleton {
//類的成員變量(一般類都有成員變量)
private byte[] data = new byte[1024];
// 未實例化的類變量
private static LazySingleton instance = null;
// 私有化構造器
private LazySingleton() {
}
public static LazySingleton getSingleton() {
// 若instance不爲null,則不用獲取鎖,提升了效率
if (instance == null) {
//同步加鎖是爲了線程安全,確保只有一個線程創建實例
synchronized (LazySingleton.class) {
//再次判空是爲了保證單例對象的唯一性,只有沒被創建纔去創建
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
//other methods
}
當兩個線程同時發現instance == null
成立時,只有一個線程有資格進入synchronized同步代碼塊完成instance的實例化,隨後的線程進入synchronized同步代碼塊後發現instance == null
不成立則無需再次實例化,以後對getSingleton方法的訪問也不需要執行synchronized同步代碼塊,大大提升了性能。滿足懶加載、線程安全、高性能這三個標準,一切看起來很完美。但這種方式在多線程環境下可能會導致空指針異常,原因如下。
首先,我們要理解new LazySingleton()
做了什麼,詳細的介紹請查看《java new一個對象的過程中發生了什麼》。本篇簡單介紹new一個對象需要的4個步驟,如下:
- 看class對象是否加載,如果沒有就先加載class對象。(加載)
- 爲類的靜態變量分配內存空間併爲其初始化默認值(連接階段),爲靜態變量賦予正確的初始值(初始化階段)。
- 調用構造函數。(單例比較複雜時,有很多的成員變量需要初始化)
- 返回地址給引用。
然後,cpu爲了優化程序,可能會進行指令重排序,打亂這3,4這幾個步驟,導致實例內存還沒分配(或是隻實例化了部分成員變量),就被使用了,導致空指針。下面舉例:
線程A執行到
new LazySingleton()
,開始初始化實例對象,由於存在指令重排序,先執行步驟4,先把引用instance
賦值了,此時還沒有執行構造函數(或執行還未完成,只實例化了部分成員變量),這時CPU時間片耗盡,切換到線程B執行,線程B調用new LazySingleton()
方法,發現instance == null
不成立,就直接返回引用地址了,然後線程B執行了一些操作,就可能導致線程B使用了還沒有被初始化的變量,報空指針錯誤。
五、Volatile + Double-Check式(最終版)
Double-Check是一種巧妙的設計,但由於JVM在運行new LazySingleton()
時可能對指令重排序,導致空指針異常。而volatile關鍵字可以防止重排序,因此還需要對Double-Check方式稍加修改。關於volatile關鍵字的用法,可參考另一篇博文《深入理解volatile關鍵字》。
// 加volatile 修飾
private static volatile LazySingleton instance = null;
至此,就有了一個線程安全、高性能、懶加載版本的雙重檢查加鎖式單例模式。但這種寫法對於初學者很棘手,一下子很難理解。
六、Holder式
直接上代碼,然後給出說明。
// 不允許被繼承
public final class HolderSingleton {
// 類的成員變量
private byte[] data = new byte[1024];
// 私有化構造器
private HolderSingleton() {
}
// 靜態內部類 Holder中持有實例instance
private static class Holder{
private static HolderSingleton instance = new HolderSingleton();
}
// 調用getSingleton,返回Holder的instance類屬性
public static HolderSingleton getSingleton() {
return Holder.instance;
}
//other methods
}
這種方式利用了類加載的特點。HolderSingleton 類中沒有持有靜態的instance實例,而是放在靜態內部類Holder中,該方式仍然需要私有化構造器。當Holder被主動引用時(懶加載)會創建HolderSingleton 的實例,JVM保證實例的唯一性,性能高。是目前廣泛採用的一種單例設計。
七、枚舉式
八、防止反射/反序列化攻擊單例類
上面的單例實現方式中,除了枚舉類型
外,其他的實現方式是可以被JAVA的反射機制攻擊的。享有特權的客戶端可以藉助AccessibleObject.setAccessible
方法通過反射機制調用私有構造器,如果需要抵禦這種攻擊,可以修改構造器,讓它的被要求創建第二個實例的時候拋出一個異常。具體方式請參考 《如何防止JAVA反射對單例類的攻擊?》 《設計模式——單例模式》
參考資料
- 《java高併發編程詳解》 汪文君
- 《Effective Java 中文版 第2版》
- 如何防止JAVA反射對單例類的攻擊?
- 單例模式爲什麼要用Volatile關鍵字
- java new一個對象的過程中發生了什麼
- 設計模式——單例模式
- 深入理解Java枚舉類型(enum)