簡介
單例指的是只能存在一個實例的類(在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操作,這會節約一定的時間。
但是,這種實現也有它的缺點:
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中它只會被創建一次。
這種實現也有一些缺點:
1. instance被創建的時機不明,任何對Singleton的調用都會提前創建instance
2. static構造函數的循環調用。如有A,B兩個類,A的靜態構造函數中調用了B,而B的靜態構造函數中又調用了A,這兩個就會形成一個循環調用,嚴重的會導致程序崩潰。
3. 我們需要手動添加Singleton的靜態構造函數來確保Singleton類型不會被自動加上beforefieldinit這個Attribute,以此來確保instance會在第一次調用Singleton時才被創建。
4. readonly的屬性無法在運行時改變,如果我們需要在程序運行時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時看到的,在這里對兩位作者表示感謝。