這篇文章主要介紹了淺談C#單例模式的實現和效能對比的相關資料,詳細的介紹了6種實現方式,需要的朋友可以參考下
簡介
單例指的是只能存在一個執行個體的類(在C#中,更準確的說法是在每個AppDomain之中只能存在一個執行個體的類,它是軟體工程中使用最多的幾種模式之一。在第一個使用者建立了這個類的執行個體之後,其後需要使用這個類的就只能使用之前建立的執行個體,無法再建立一個新的執行個體。通常情況下,單例會在第一次被使用時建立。本文會對C#中幾種單例的實現方式進行介紹,並分析它們之間的執行緒安全性和效能差異。
單例的實現方式有很多種,但從最簡單的實現(非消極式載入,非安全執行緒,效率低下),到可消極式載入,安全執行緒,且高效的實現,它們都有一些基本的共同點:
幾種實現
一非安全執行緒
//Bad code! Do not use!public sealed class Singleton{ private static Singleton instance = null; private Singleton() { } public static Singleton instance { get { if (instance == null) { instance = new Singleton(); } return instance; } }}
這種方法不是安全執行緒的,會存在兩個線程同時執行if (instance == null)並且建立兩個不同的instance,後建立的會替換掉新建立的,導致之前拿到的reference為空白。
二簡單的安全執行緒實現
public sealed class Singleton{ private static Singleton instance = null; private static readonly object padlock = new object(); Singleton() { } public static Singleton Instance { get { lock (padlock) { if (instance == null) { instance = new Singleton(); } return instance; } } }}
相比較於實現一,這個版本加上了一個對instance的鎖,在調用instance之前要先對padlock上鎖,這樣就避免了實現一中的線程衝突,該實現自始至終只會建立一個instance了。但是,由於每次調用Instance都會使用到鎖,而調用鎖的開銷較大,這個實現會有一定的效能損失。
注意這裡我們使用的是建立一個private的object執行個體padlock來實現鎖操作,而不是直接對Singleton進行上鎖。直接對類型上鎖會出現潛在的風險,因為這個類型是public的,所以理論上它會在任何code裡調用,直接對它上鎖會導致效能問題,甚至會出現死結情況。
Note: C#中,同一個線程是可以對一個object進行多次上鎖的,但是不同線程之間如果同時上鎖,就可能會出現線程等待,或者嚴重的會出現死結情況。因此,我們在使用lock時,盡量選擇類中的私人變數上鎖,這樣可以避免上述情況發生。
三雙步驟驗證的安全執行緒實現
public sealed calss Singleton{ private static Singleton instance = null; private static readonly object padlock = new object(); Singleton() { } public static Singleton Instance { get { if (instance == null) { lock (padlock) { if (instance == null) { instance = new Singleton(); } } } return instance; } } }
在保證安全執行緒的同時,這個實現還避免了每次調用Instance都進行lock操作,這會節約一定的時間。
但是,這種實現也有它的缺點:
1無法在Java中工作。(具體原因可以見原文,這邊沒怎麼理解)
2程式員在自己實現時很容易出錯。如果對這個模式的代碼進行自己的修改,要倍加小心,因為double check的邏輯較為複雜,很容易出現思考不周而出錯的情況。
四不用鎖的安全執行緒實現
public sealed class Singleton{ //在Singleton第一次被調用時會執行instance的初始化 private static readonly Singleton instance = new Singleton(); //Explicit static consturctor to tell C# compiler //not to mark type as beforefieldinit static Singleton() { } private Singleton() { } public static Singleton Instance { get { return instance; } }}
這個實現很簡單,並沒有用到鎖,但是它仍然是安全執行緒的。這裡使用了一個static,readonly的Singleton執行個體,它會在Singleton第一次被調用的時候建立一個instance,這裡建立時候的安全執行緒保障是由.NET直接控制的,我們可以認為它是一個原子操作,並且在一個AppDomaing中它只會被建立一次。
這種實現也有一些缺點:
1instance被建立的時機不明,任何對Singleton的調用都會提前建立instance
2static建構函式的迴圈調用。如有A,B兩個類,A的靜態建構函式中調用了B,而B的靜態建構函式中又調用了A,這兩個就會形成一個迴圈調用,嚴重的會導致程式崩潰。
3我們需要手動添加Singleton的靜態建構函式來確保Singleton類型不會被自動加上beforefieldinit這個Attribute,以此來確保instance會在第一次調用Singleton時才被建立。
4readonly的屬性無法在運行時改變,如果我們需要在程式運行時dispose這個instance再重新建立一個新的instance,這種實現方法就無法滿足。
五完全消極式載入實現(fully lazy instantiation)
public sealed class Singleton{ private Singleton() { } public static Singleton Instance { get { return Nested.instance; } } private class Nested { // Explicit static constructor to tell C# compiler // not to mark type as beforefieldinit static Nested() { } internal static readonly Singleton instance = new Singleton(); }}
實現五是實現四的封裝。它確保了instance只會在Instance的get方法裡面調用,且只會在第一次調用前初始化。它是實現四的確保消極式載入的版本。
六 使用.NET4的Lazy<T>類型
public sealed class Singleton{ private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton()); public static Singleton Instance { get { return lazy.Value; } } private Singleton() { }}
.NET4或以上的版本支援Lazy<T>來實現消極式載入,它用最簡潔的代碼保證了單例的安全執行緒和消極式載入特性。
效能差異
之前的實現中,我們都在強調代碼的執行緒安全性和消極式載入。然而在實際使用中,如果你的單例類的初始化不是一個很耗時的操作或者初始化順序不會導致bug,延遲初始化是一個可有可無的特性,因為初始化所佔用的時間是可以忽略不計的。
在實際使用情境中,如果你的單例執行個體會被頻繁得調用(如在一個迴圈中),那麼為了保證安全執行緒而帶來的效能消耗是更值得關注的地方。
為了比較這幾種實現的效能,我做了一個小測試,迴圈拿這些實現中的單例9億次,每次調用instance的方法執行一個count++操作,每隔一百萬輸出一次,運行環境是MBP上的Visual Studio for Mac。結果如下:
|
執行緒安全性 |
消極式載入 |
測試回合時間(ms) |
實現一 |
否 |
是 |
15532 |
實現二 |
是 |
是 |
45803 |
實現三 |
是 |
是 |
15953 |
實現四 |
是 |
不完全 |
14572 |
實現五 |
是 |
是 |
14295 |
實現六 |
是 |
是 |
22875 |
測試方法並不嚴謹,但是仍然可以看出,方法二由於每次都需要調用lock,是最耗時的,幾乎是其他幾個的三倍。排第二的則是使用.NET Lazy類型的實現,比其他多了二分之一左右。其餘的四個,則沒有明顯區別。
總結
總體來說,上面說的多種單例實現方式在現今的電腦效能下差距都不大,除非你需要特別大並發量的調用instance,才會需要去考慮鎖的效能問題。
對於一般的開發人員來說,使用方法二或者方法六來實現單例已經是足夠好的了,方法四和五則需要對C#運行流程有一個較好的認識,並且實現時需要掌握一定技巧,並且他們節省的時間仍然是有限的。
引用
本文大部分是翻譯自Implementing the Singleton Pattern in C#,加上了一部分自己的理解。這是我搜尋static readonly field initializer vs static constructor initialization時看到的,在這裡對兩位作者表示感謝。