本文將介紹如何使用C#語言實現餓漢式單例與懶漢式單例,並分析餓漢式單例與懶漢式單例的優缺點。
1. 餓漢式單例類
餓漢式單例類(Eager Singleton)是實現起來最容易的單例類,餓漢式單例類結構圖1所示。
圖1 餓漢式單例類圖
從圖1中可以看出,由於在定義靜態變數的時候執行個體化單例類,因此在類載入時單例對象就已建立,代碼如下:
class EagerSingleton { private static EagerSingleton instance = new EagerSingleton(); private EagerSingleton() { } public static EagerSingleton GetInstance() { return instance; }}
當類被載入時,靜態變數instance 會被初始化,此時類的私人建構函式會被調用,單例類的唯一執行個體將被建立。
2. 懶漢式單例類與雙重檢查鎖定
與餓漢式單例類相同之處是,懶漢式單例類(Lazy Singleton)的建構函式也是私人的。與餓漢式單例類不同的是,懶漢式單例類在第一次被引用時將自己執行個體化,在懶漢式單例類被載入時不會將自己執行個體化。懶漢式單例類結構圖2所示。
圖2 懶漢式單例類圖
從圖2可以看出,在懶漢式單例類中,不是在定義靜態變數時執行個體化單例類,而是在第一次調用靜態Factory 方法時執行個體化單例類。
但是懶漢式單例存在一個很嚴重的問題:如果在高並發、多線程環境下實現懶漢式單例類,在某一時刻可能會有多個線程需要使用單例對象,即會有多個線程同時調用GetInstance()方法,可能會造成建立多個執行個體對象,這將違背單例模式的設計意圖。為了防止產生多個單例對象,需要使用C#語言中的lock關鍵字,lock關鍵字鎖定的程式碼片段稱之為臨界區,可以確保當一個線程位於代碼的臨界區時,另一個線程不能進入臨界區。如果其他線程試圖進入鎖定的代碼,則將一直等待,直到該對象被釋放為止。修改之後的懶漢式單例類代碼如下:
class LazySingleton { private static LazySingleton instance = null; //程式運行時建立一個靜態唯讀輔助對象 private static readonly object syncRoot = new object(); private LazySingleton() { } public static LazySingleton GetInstance() { //第一重判斷,先判斷執行個體是否存在,不存在再加鎖處理 if (instance == null) { //加鎖的程式在某一時刻只允許一個線程訪問 lock(syncRoot) { //第二重判斷 if(instance==null) { instance = new LazySingleton(); //建立單例執行個體 } } } return instance; }}
在上面給出的懶漢式單例類實現代碼中,對靜態Factory 方法GetInstance()中建立單例對象的代碼進行了加鎖,由於在調用時無法確定該單例對象是否已建立,因此需要使用輔助對象syncRoot來進行代碼鎖定。為了不影響程式的效能,此處只鎖定建立單例對象的代碼,並未鎖定整個方法。如果執行個體存在則直接返回,如果執行個體未建立則加鎖後再建立。
為了更好地對單例對象的建立進行控制,此處使用了一種被稱之為雙重檢查鎖定(Double-CheckLocking)的雙重判斷機制。在雙重檢查鎖定中,當執行個體不存在且同時有兩個線程調用GetInstance()方法時,它們都可以通過第一重“instance==null”判斷,然後由於lock鎖定機制,只有一個線程進入lock中執行建立代碼,另一個線程處於排隊等待狀態,必須等待第一個線程執行完畢後才可以進入lock鎖定的代碼,如果此時不進行第二重“instance==null”判斷,第二個線程並不知道執行個體已經建立,將繼續建立新的執行個體,還是會產生多個單例對象,違背單例模式的設計思想,因此需要進行雙重檢查。
3. 餓漢式單例類與懶漢式單例類比較
餓漢式單例類在類被載入時就將自己執行個體化,它的優點在於無須考慮多個線程同時訪問的問題,可以確保執行個體的唯一性;從調用速度和反應時間角度來講,由於單例對象一開始就得以建立,因此要優於懶漢式單例。但是無論系統在運行時是否需要使用該單例對象,由於在類載入時該對象就需要建立,因此從資源利用效率角度來講,餓漢式單例不及懶漢式單例,而且在系統載入時由於需要建立餓漢式單例對象,載入時間可能會比較長。
懶漢式單例類在第一次使用時建立,無須一直佔用系統資源,實現了消極式載入,但是必須處理好多個線程同時訪問的問題,特別是當單例類作為資源控制器,在執行個體化時必然涉及資源初始化,而資源初始化很有可能耗費大量時間,這意味著出現多線程同時首次引用此類的機率變得較大,需要通過雙重檢查鎖定等機制進行控制,這將導致系統效能受到一定影響。
Java程式員可進一步閱讀:確保對象的唯一性——單例模式 (三)與 確保對象的唯一性——單例模式 (四)
【作者:劉偉 http://blog.csdn.net/lovelion】