單例指的是只能存在一個實例的類(在C#中,更準確的說法是在每一個AppDomain之中只能存在一個實例的類,它是軟件工程中使用最多的幾種模式之一。在第一個使用者建立了這個類的實例以後,其後須要使用這個類的就只能使用以前建立的實例,沒法再建立一個新的實例。一般狀況下,單例會在第一次被使用時建立。本文會對C#中幾種單例的實現方式進行介紹,並分析它們之間的線程安全性和性能差別。程序員
單例的實現方式有不少種,但從最簡單的實現(非延遲加載,非線程安全,效率低下),到可延遲加載,線程安全,且高效的實現,它們都有一些基本的共同點:安全
. 單例類都只有一個private的無參構造函數
. 類聲明爲sealed(不是必須的)
. 類中有一個靜態變量保存着所建立的實例的引用
. 單例類會提供一個靜態方法或屬性來返回建立的實例的引用(eg.GetInstance)
複製代碼
//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操做,這會節約必定的時間。spa
可是,這種實現也有它的缺點:線程
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中它只會被建立一次。翻譯
這種實現也有一些缺點:code
1. instance被建立的時機不明,任何對Singleton的調用都會提早建立instance
2. static構造函數的循環調用。若有A,B兩個類,A的靜態構造函數中調用了B,而B的靜態構造函數中又調用了A,這兩個就會造成一個循環調用,嚴重的會致使程序崩潰。
3. 咱們須要手動添加Singleton的靜態構造函數來確保Singleton類型不會被自動加上beforefieldinit這個Attribute,以此來確保instance會在第一次調用Singleton時才被建立。
4. readonly的屬性沒法在運行時改變,若是咱們須要在程序運行時dispose這個instance再從新建立一個新的instance,這種實現方法就沒法知足。
複製代碼
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方法裏面調用,且只會在第一次調用前初始化。它是實現四的確保延遲加載的版本。
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來實現延遲加載,它用最簡潔的代碼保證了單例的線程安全和延遲加載特性。
以前的實現中,咱們都在強調代碼的線程安全性和延遲加載。然而在實際使用中,若是你的單例類的初始化不是一個很耗時的操做或者初始化順序不會致使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時看到的,在這裏對兩位做者表示感謝。