1. 程式人生 > >【設計模式基礎】建立型模式

【設計模式基礎】建立型模式

1. 模式意圖

保證類僅有一個例項,並提供一個訪問它的全域性訪問點。

2. 模式定義


Singleton: 定義一個Instance操作,允許客戶訪問它的唯一例項。Instance是一個類操作;可能負責建立它自己的唯一例項;客戶只能通過Singleton的Instance操作訪問一個Singleton的例項。

3. 模式實現

3.1 Java實現

當設計一個單件類的時候,會在類的內部構造這個類,並對外提供一個static getInstance方法提供獲取單件物件的途徑:

public class Singleton
{
    private static Singleton instance = new Singleton();
	
	private Singleton()
	{
	   ... ...
	}
	
	public static Singleton getInstance()
	{
	    return instance;
	}
}

這樣程式碼的缺點是:第一次載入類的時候會連帶著建立Singleton例項,這樣的結構與我們期望的不同,因為建立例項的時候可能並不是我們需要這個例項的時候。同時,如果這個Singleton例項的建立非常消耗系統資源,而應用始終都沒有使用Singleton例項,那麼建立Singleton消耗的系統資源就被白白浪費了。

為了避免這種情況,通常使用惰性載入的機制,也就是在使用的時候才去建立。

惰性載入

public class Singleton
{
    private static Singleton instance = null;
	
	private Singleton()
	{
	   ... ...
	}
	
	public static Singleton getInstance()
	{
	    if (instance == null)
		{
		    instance = new Singleton();
		}
		
	    return instance;
	}
}

這樣當我們第一次呼叫Singleton.getInstance()的時候,這個單件才被建立.

惰性載入在多執行緒中的問題:

如果兩個執行緒A和B同時執行了getInstance方法,然後以如下方式執行:

  1. A進入if判斷,此時instance為null,進入if內
  2. B進入if判斷,此時A還沒有建立instance,因此B也進入if內
  3. A建立一個instance並返回
  4. B建立一個instance並返回

此時問題出現了,我們的單件被建立了兩次,而這並不是我們所期望的。

各種解決方案及其存在的問題

1. 使用class鎖

給getInstance方法加上一個synchronized字首,這樣每次只允許一個執行緒呼叫getInstance方法:

public class Singleton
{
    private static Singleton instance = null;
	
	private Singleton()
	{
	   ... ...
	}
	
	public static synchronized Singleton getInstance()
	{
	    if (instance == null)
		{
		    instance = new Singleton();
		}
		
	    return instance;
	}
}

這種解決辦法的確可以防止錯誤的出現,但是它卻很影響效能:每次呼叫getInstance方法的時候都必須獲得Singleton的鎖,而實際上,當單件例項被建立以後,其後的請求就沒有必要再使用互斥機制了。

2. double-checked locking雙重檢查加鎖

public class Singleton
{
    private volatile static Singleton instance = null;
	
	private Singleton()
	{
	   ... ...
	}
	
	public static  Singleton getInstance()
	{
	    if (instance == null)
		{
		    synchronized(Singleton.class)
			{
			    if (instance == null)
				{
		            instance = new Singleton();
				}
			}
		}
		
	    return instance;
	}
}

首先當一個執行緒發出請求後,會先檢查instance是否null,如果不是則直接返回其內容,這樣避免進入synchronized塊所需要花費的資源。其次,兩個執行緒同時進入第一個if判斷,那麼他們也必須按照順序執行synchronized塊中的程式碼,第一個進入程式碼塊的執行緒會建立一個新的Singleton例項,而後續的執行緒則因為無法通過if判斷,而不會建立多餘的例項。

但實際上,從JVM的角度講,這些程式碼仍然可能發生錯誤:

對於JVM而言,它執行的是一個個java指令。在Java指令中建立物件和賦值操作時分開進行的,也就是說instance = new Singleton();語句是分兩步執行的。但是JVM並不保證這兩個操作的先後順序,也就是說有可能JVM會為新的Singleton例項分配空間,然後直接賦值給instance成員,然後再去初始化這個Singleton例項。這樣就是出錯成為了可能,例如:

  1. A、B執行緒同時進入第一個if判斷
  2. A首先進入synchronized塊,由於instance為null,所以它執行instance = new Singleton();
  3. 由於JVM內部的優化機制,JVM先畫出了一些分配給Singleton例項的空白記憶體,並賦值給instance成員(此時JVM沒有開始初始化這個例項),然後A離開了synchronized塊;
  4. B進入synchronized塊,由於instance此時不是null,因此它馬上離開了synchronized塊並將結果返回給呼叫該方法的程式;
  5. 此時B執行緒打算使用Singleton例項,卻發現它沒有被初始化,優勢錯誤發生了;

3. 通過內部類實現多執行緒環境中的單件模式

public class Singleton
{
	private Singleton()
	{
	   ... ...
	}
	
	private static class SingletonContainer
	{
        private static Singleton instance = new Singleton();
	}
	
	public static Singleton getInstance()
	{
	    return SingletonContainer.instance;
	}
}

JVM的內部機制能夠保證當一個類被載入的時候,這個類的載入過程是執行緒互斥的。這樣當我們第一次呼叫getInstance的時候,JVM能夠幫我們保證instance只被建立一次,並且會保證把賦值給instance的記憶體初始化完畢。此外該方法也只會在第一次呼叫的時候使用互斥機制,這樣就解決了低效問題。最後instance是在第一次載入SingletonContainer類時被建立的,而SingletonContainer類則在呼叫getInstance方法的時候才會被載入,因此也實現了惰性載入。

3.2 C++實現

3.3 C#實現

4. 模式應用

單件模式的應用:

  • 每臺計算機可以有若干印表機,但只能有一個Printer Spooler,避免兩個列印作業同時輸出到印表機;
  • 一個具有自動編號主鍵的表可以有多個使用者同時使用,但資料庫中只能有一個地方分配下一個主鍵編號,否則會出現主鍵重複;