設計模式--單例Singleton

用途:

確保一個類只有一個實例,並提供對其的全局訪問點。從而保證數據內容的一致性,節省內存資源。

例如,Windows 中只能打開一個任務管理器,這樣可以避免因打開多個任務管理器窗口而出現各個窗口顯示內容不一致的情況,或造成內存資源的浪費。每臺計算機可以有若干通信端口,系統應當集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調用。

在計算機系統中,多線程中的線程池、顯卡的驅動程序對象、打印機的後臺處理服務、應用程序的日誌對象、數據庫的連接池、網站的計數器、Web 應用的配置對象、應用程序中的對話框、系統中的緩存等常被設計成單例。這些應用都或多或少具有資源管理器的功能。總之,選擇單例模式就是爲了避免不一致狀態。

定義:

單例(Singleton)模式的定義:指一個類只有一個實例,且該類能自行創建這個實例的一種模式。

特點:

  1. 單例類只有一個實例對象;
  2. 該單例對象必須由單例類自行創建;
  3. 單例類對外提供一個訪問該單例的全局訪問點;

代碼實現:

讓類的構造函數私有,在類內創建一個靜態對象,並創建一個公有的靜態方法訪問這個對象。

寫法上有2種方式:

[1]立即加載方式, 也叫“餓漢模式”

單例在類加載初始化時就創建好一個靜態的對象供外部使用,除非系統重啓,否則這個對象不會改變,不同線程來調用getInstance()的時候,獲取的都是類初始化就創建的同一個實例,所以本身就是線程安全的。

//餓漢式單例類.在類初始化時,已經自行實例化(線程安全)
public class Singleton {

    private static Singleton ss = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return ss;
    }
}

唯一的缺點就是實例創建過早,類初始化就創建了,還沒調用就已經存在了,容易造成內存資源浪費。

[2]延遲加載方式, 也叫“懶漢模式”

餓漢是類一旦加載,就把單例初始化完成,保證調用getInstance()的時候,單例是已經存在的;

而懶漢比較懶,只有當調用getInstance()的時候,纔會去初始化這個單例。

//懶漢式單例類.在第一次調用的時候實例化自己(非線程安全)
public class LazySingleton {

    private static LazySingleton ls = null;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (ls == null) {
            ls = new LazySingleton();
        }
        return ls;
    }
}

但是這種懶漢式單例的實現沒有考慮線程安全問題,它是線程不安全的。

爲什麼說這種代碼寫法線程不安全呢?

假設LazySingleton類剛剛被初始化,ls對象還是空,這時候兩個線程A和B同時訪問getInstance方法:

因爲ls是null,所以A和B兩個線程同時通過了條件判斷,開始執行new操作,顯然ls被構建了兩次。

要保證懶漢式的線程安全,有下面3種方法。

(1)使getInstance()同步

//懶漢式單例類.在第一次調用的時候實例化自己(線程安全 -- 同步法)
public class LazySingleton {

    private static volatile LazySingleton ls = null; //保證ls在所有線程中同步

    private LazySingleton() {}

    public static synchronized LazySingleton getInstance() {//getInstance 方法前加同步
        if (ls == null) {
            ls = new LazySingleton();
        }
        return ls;
    }
}

在方法調用上加了同步,雖然線程安全了,但是每次都要同步,會影響性能,畢竟大多數情況下是不需要同步的;

(2)雙重檢查鎖定DCL(double checked locking)

//懶漢式單例類.在第一次調用的時候實例化自己(非絕對線程安全[取決於編譯器] -- 雙重檢查鎖定)
public class LazySingleton {

    private static LazySingleton ls = null;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (ls == null) {//雙重檢查
   	    	synchronized (LazySingleton.class) {  //鎖住整個類  
            	if (ls == null) { //雙重檢查
                	ls = new LazySingleton();   
            	}    
            } 
        }
        return ls;
    }
}

在getInstance中做了兩次null檢查,第一次是爲了提高運行效率;第二次是爲了進行同步,避免多線程問題,進入synchronized臨界區以後,還要再做一次判空。因爲當兩個線程同時訪問的時候,線程A構建完對象,線程B也已經通過了第一次的判空驗證,不做第二次判空的話,線程B還是會再次構建ls對象。

雖然上面這樣寫邏輯上看沒什麼問題,但是和編譯器有關,不算絕對的線程安全。

以java爲例,需要了解JVM編譯器的指令重排

當AB兩線程同時調用getInstance,A執行到new語句,B準備執行判斷。

什麼叫指令重排呢,比如A在執行語句ls = new LazySingleton(),看起來是一句話,但這並不是一個原子操作(要麼全部執行完,要麼全部不執行,不能執行一半),這句話被編譯成8條彙編指令,大致做了3件事情:

  1. 給LazySingleton的實例分配內存。
  2. 初始化LazySingleton的構造器
  3. 將LazySingleton對象指向分配的內存空間(注意到這步ls就非null了)。

由於Java編譯器允許處理器亂序執行(out-of-order),以及JDK1.5之前JMM(Java Memory Model)中Cache、寄存器到主內存回寫順序的規定,上面的第二點和第三點的順序是無法保證的,也就是說,執行順序可能是1-2-3也可能是1-3-2,如果是1-3-2,並且在3執行完畢、2未執行之前,被切換到線程B上,這時候LazySingleton因爲已經在線程A內執行過了第三點,ls已經是非空了,所以線程B直接拿走ls,然後使用,然後順理成章地報錯,而且這種難以跟蹤難以重現的錯誤估計調試上一星期都未必能找得出來。

DCL的寫法來實現單例是很多技術書、教科書(包括基於JDK1.4以前版本的書籍)上推薦的寫法,實際上是不完全正確的。的確在一些語言(譬如C語言)上DCL是可行的,取決於是否能保證2、3步的順序。在JDK1.5之後,官方已經注意到這種問題,因此調整了JMM、具體化了volatile關鍵字,因此如果JDK是1.5或之後的版本,只需要將ls的定義加上volatile關鍵字,就可以禁止編譯器重排序,就可以使用DCL的寫法來完成單例模式。所以JDK1.5以後可以加上volatile來實現DCL方式的絕對線程安全。

//懶漢式單例類.在第一次調用的時候實例化自己(線程安全 -- 雙重檢查鎖定)
public class LazySingleton {

    private static volatile LazySingleton ls = null; //volatile禁止指令重排

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (ls == null) {//雙重檢查
   	    	synchronized (LazySingleton.class) {  //鎖住整個類  
            	if (ls == null) { //雙重檢查
                	ls = new LazySingleton();   
            	}    
            } 
        }
        return ls;
    }
}

DCL方式要小心使用,需要了解具體的語言編譯器,在禁止編譯器指令重排後,才能保證絕對線程安全。

(3)靜態內部類

//懶漢式單例類.在第一次調用的時候實例化自己(線程安全 -- 靜態內部類)
public class LazySingleton {

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        return InnerClass.ls;
    }

    private static class InnerClass {
        private static final LazySingleton ls = new LazySingleton();
    }
}

利用了classloader的機制來保證初始化單例時只有一個線程,所以也是線程安全的。

破壞單例模式:

1. 反射

以最簡單而且線程安全的"餓漢模式"來進行測試。

//獲得構造器
Constructor con = Singleton.class.getDeclaredConstructor();
//設置爲可訪問
con.setAccessible(true);
//構造兩個不同的對象
Singleton singletonA = (Singleton)con.newInstance();
Singleton singletonB = (Singleton)con.newInstance();
//驗證是否是相同對象
System.out.println(singletonA.equals(singletonB));

最後的比較結果是false,反射可以訪問類裏面所有的私有屬性和方法。所以反射訪問私有構造器是可以非常容易的創建多個對象實例,從而破壞單例模式。簡單的處理方法就是在私有構造器裏面進行判斷,禁止進行反射,但是也僅限於餓漢的寫法,懶漢的還是不能避免。

//餓漢式單例類.線程安全.避免反射創建
public class Singleton {

    private static Singleton ss = new Singleton();

    //避免反射和多類加載器破壞
    private Singleton() {
		if (Singleton.ss != null) {
        	throw new Exception("Singleton can not use Reflection");
    	}
    }

    public static Singleton getInstance() {
        return ss;
    }
}

2.序列化和反序列化

還以最簡單而且線程安全的"餓漢模式"寫法來進行測試。

Singleton instanceA = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new  		FileOutputStream("sersingle_file"));
oos.writeObject(instanceA);

File file = new File("sersingle_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton instanceB = (Singleton) ois.readObject();
System.out.println(singletonA.equals(singletonB));

最後的比較結果是false,說明兩個對象不一樣,所以序列化和反序列化也破壞單例模式。解決方法是在單例中添加readResolve方法。

//餓漢式單例類.避免反序列破壞
public class Singleton implements Serializable{

    private static Singleton ss = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return ss;
    }

    //避免反序列破壞
    protected Object readResolve() {
        return ss;
    }
}

最佳方法:

枚舉

public enum EnumSingleton {
  INSTANCE;
}

EnumSingleton enumSingletonA = EnumSingleton.INSTANCE;
EnumSingleton enumSingletonB = EnumSingleton.INSTANCE;
assertEquals(enumSingletonA, enumSingletonB); // true

Joshua Bloch, Effective Java 2nd Edition p.18

A single-element enum type is the best way to implement a singleton

單元素枚舉類型是實現單例的最佳方法

有了enum類型修飾,JVM會阻止反射強行構造對象,而且可以在對象被反序列化的時候,保證反序列結果返回同一對象,並且是線程安全的。唯一的不足就是不是延遲加載,單例對象是在枚舉類被初始化加載的時候就進行創建了。

JAVA應用案例:

java.lang.Runtime#getRuntime()

java.awt.Desktop#getDesktop()

java.lang.System#getSecurityManager()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章