【設計模式基礎】建立型模式
阿新 • • 發佈:2019-02-10
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方法,然後以如下方式執行:
- A進入if判斷,此時instance為null,進入if內
- B進入if判斷,此時A還沒有建立instance,因此B也進入if內
- A建立一個instance並返回
- 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例項。這樣就是出錯成為了可能,例如:
- A、B執行緒同時進入第一個if判斷
- A首先進入synchronized塊,由於instance為null,所以它執行instance = new Singleton();
- 由於JVM內部的優化機制,JVM先畫出了一些分配給Singleton例項的空白記憶體,並賦值給instance成員(此時JVM沒有開始初始化這個例項),然後A離開了synchronized塊;
- B進入synchronized塊,由於instance此時不是null,因此它馬上離開了synchronized塊並將結果返回給呼叫該方法的程式;
- 此時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,避免兩個列印作業同時輸出到印表機;
- 一個具有自動編號主鍵的表可以有多個使用者同時使用,但資料庫中只能有一個地方分配下一個主鍵編號,否則會出現主鍵重複;