單例模式的多種實現

單例模式

單例模式來確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。
單例模式確保一個類只有一個實例,並提供一個全局訪問點。

單例模式是一種對象創建型模式。
單例模式又被稱爲單件模式或單態模式。
單例模式的要點有三個:

  1. 某個類只能有一個實例
  2. 必須自行創建這個實例
  3. 必須自行向整個系統提供這個實例

單例模式的經典實現

public class Singleton {
    private static Singleton uniqueInstance;//利用一個靜態變量來記錄Singleton類的唯一實例
    //這裏有其它的有用實例化變量
    
    private Singleton(){};//私有構造器,只有自己纔可new自己

    public static Singleton getInstance(){
        if (uniqueInstance == null){//如果uniqueInstance是空的,表示還沒有創建實例
            uniqueInstance = new Singleton();//如果uniqueInstance不存在,我們就調用私有構造器生成一個實例
            //並把它賦值給uniqueInstance
            //如果我們不需要這個實例,那麼這個實例就永遠不會產生,這就是延遲實例化
        }
        return uniqueInstance;
    }

    public static void operation(){//類中的其它有用的方法,最好是static的
    }
}

模式分析:

  • 單例類擁有一個私有構造函數,確保用戶無法通過new關鍵字直接實例化它。
  • 該模式還包含一個靜態私有成員變量與靜態共有的工廠方法,該工廠方法負責檢驗實例的存在性並延遲實例化自己,然後存儲在靜態成員的變量中,以確保只有一個實例被創建。

多線程情況下的單例模式

上述單例模式的經典實現,在多線程的情況下是有問題的。
Singleton.png

在當線程1還未new 出Singleton對象時線程2判斷爲true,這樣就會導致創建了倆個Singleton對象。
如何避免這種情況呢,只要把getinstance()變成同步方法,多線程的災難就輕而易舉的解決了,如下所示:

    public static synchronized Singleton getInstance(){
        if (uniqueInstance == null){
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }

如果你的應用程序可以接受getInstance()造成的額外負擔,直接在方法上加上synchronized 關鍵字是最簡單的實現方式,這可能會造成程序的執行效率大大下降,如果將getInstance()的程序使用在頻繁運行的地方,就必須得重新考慮了。

懶漢式-雙重檢查加鎖

順着上述直接加synchronized 關鍵字同步的思路,我們可以將加鎖的範圍儘量縮小。

	public static Singleton getInstance(){
    	if(instance == null) {//判斷是否有Singleton實例對象
    		synchronized (Singleton.class) {//類鎖  A處
				if(instance == null) {//再次進行判斷  B處
					instance = new Singleton();//如果沒有進行創建  C處
				}
			}
    	}
        return instance ;
    }

雙重檢查鎖定背後的理論是:在 B處的第二次檢查可以使得創建兩個不同的 Singleton 對象成爲不可能。假設現在有倆個線程,產生了如下所示的事件序列:

  1. 線程 1 進入 getInstance() 方法。
  2. 由於 instance 爲 null,線程 1 在 A處進入 synchronized 塊,獲取Singleton類鎖。
  3. 線程 1 被線程 2 預佔。
  4. 線程 2 進入 getInstance() 方法。
  5. 由於 instance 仍舊爲 null,線程 2 試圖獲取 A處的鎖。然而,由於線程 1 持有該鎖,線程 2 在 A處阻塞。
  6. 線程 2 被線程 1 預佔。
  7. 線程 1 執行,由於在 B 處實例仍舊爲 null,線程 1 還創建一個 Singleton 對象並將其引用賦值給 instance。
  8. 線程 1 退出 synchronized 塊並從 getInstance() 方法返回實例。
  9. 線程 1 被線程 2 預佔。
  10. 線程 2 獲取 A 處的鎖並檢查 instance 是否爲 null。
  11. 由於 instance 是非 null 的,並沒有創建第二個 Singleton 對象,由線程 1 創建的對象被返回。

雙重檢查鎖定背後的理論是完美的。不幸地是,現實完全不同。

雙重檢查鎖定的問題是:並不能保證它會在單處理器或多處理器計算機上順利運行。
雙重檢查鎖定失敗的問題並不歸咎於 JVM 中的實現 bug,而是歸咎於 Java 平臺內存模型。內存模型允許所謂的“無序寫入”,這也是這些習語失敗的一個主要原因。

無序寫入
爲解釋該問題,需要重新考察上述清單 4 中的C 行。此行代碼創建了一個 Singleton 對象並初始化變量 instance 來引用此對象。這行代碼的問題是:在 Singleton 構造函數體執行之前,變量 instance 可能成爲非 null 的。

什麼?這一說法可能讓您始料未及,但事實確實如此。在解釋這個現象如何發生前,請先暫時接受這一事實,我們先來考察一下雙重檢查鎖定是如何被破壞的。
假設清單 4 中代碼執行以下事件序列:

  1. 線程 1 進入 getInstance() 方法。
  2. 由於 instance 爲 null,線程 1 在 A 處進入 synchronized 塊。
  3. 線程 1 前進到C 處,但在構造函數執行之前,使實例成爲非 null。
  4. 線程 1 被線程 2 預佔。
  5. 線程 2 檢查實例是否爲 null。因爲實例不爲 null,線程 2 將 instance 引用返回給一個構造完整但部分初始化了的 Singleton對象。
  6. 線程 2 被線程 1 預佔。
  7. 線程 1 通過運行 Singleton 對象的構造函數並將引用返回給它,來完成對該對象的初始化。

對象的初始化,並不是有序的,並不是一次性完成的。
一種可能的情況是,生成了一個實例,但還未將該實例的屬性值初始化(即還未執行構造方法),而這時instance 已經不爲null。所以上述事件序列線程2還未等到線程1將Singleton對象完全初始化完成,得知instance不爲null,便把這個不完全的instance 返回了。線程1繼續執行初始化,然後將完全實例化的instance 返回。這樣線程1與線程2便返回了倆個不完全相同的實例對象。

解決雙重檢查加鎖的問題

解決Singleton 實例對象在不完全初始化的情況下返回而產生了倆種實例的情況,只需要在private volatile static Singleton instance;加一個volatile 關鍵字即可。這樣就保證了instance對象在多個線程之間的可見性。

但是,現在雙重檢查加鎖的這種方式,現在已經不推薦使用了,那麼現在如何來實現線程安全的單例模式呢?

餓漢式-類初始化模式

public class SingleEHan {
    private static final SingleEHan singleEHan = new SingleEHan();
    private SingleEHan(){}

    public static SingleEHan getInstance(){
        return singleEHan;
    }
}

在JVM中,對類的加載和初始化,由虛擬機保證線程安全。

但是如果SingleEHan 這個類很大的話,可能會佔據很多的內存空間。可以考慮下面的懶漢式-類初始化模式。

懶漢式-類初始化模式/延遲佔位模式

public class SingleInit {
    private SingleInit(){}

    //定義一個私有類,來持有當前類的實例
    private static class InstanceHolder{
        public static SingleInit instance = new SingleInit();
    }

    public static SingleInit getInstance(){
        return InstanceHolder.instance;
    }
}

該實現方式,同樣也是利用了JVM保證了類的加載和初始化時的線程安全。
JVM在對SingleInit 進行加載的時候,是不會對私有類進行初始化的,只有當調用getInstance()方法時,JVM纔會將私有類初始化,並由私有類來代替,返回實例出去。

延遲佔位模式其實是個很用處很廣泛的模式,下面舉個栗子~

public class InstanceLazy {
	private Integer value;
	private Integer val ;//可能是一個很大的對象,也可能是一個非常大的數組,但是這個屬性可能平時用的不是很多,所以可以在需要這個屬性val的時候,再將其進行初始化,採用延遲佔位同時也保證了線程安全。
	
    public InstanceLazy(Integer value) {
		this.value = value;
	}

	public Integer getValue() {
		return value;
	}

	private static class ValHolder {
		public static Integer vHolder = new Integer(1000000);
	}

	public Integer getVal() {
		return ValHolder.vHolder;
	}
}

單例模式的優缺點

優點:

  1. 提供了對唯一實例的受控訪問。因爲單例類封裝了它的唯一實例,所以它可以嚴格控制客戶怎樣以及何時訪問它。
  2. 由於在系統內存中只存在一個對象,因此可以節約系統資源,對於一些需要頻繁創建和銷燬的對象,單例模式無疑可以提高系統的性能。

缺點:

  1. 由於單例模式中沒有抽象層,因此單例類的擴展有很大的困難
  2. 單例類的職責過重,在一定程度上違背了“單一職責原則”。因爲單例類即充當了工廠角色,提供了工廠方法,又充當了產品角色,包含一些業務方法,將產品的創建和產品本身的功能融合在了一起。

單例模式的適用環境

  • 系統只需要一個實例對象的時候
  • 系統需要考慮資源消耗太大隻允許創建一個對象
  • 客戶端調用類的單個實例只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該實例。

參考

  • 《Head First 設計模式》
  • 《軟件體系結構與設計》
  • https://blog.csdn.net/chenchaofuck1/article/details/51702129
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章