C#單例模式的實現和性能對比

簡介

單例指的是只能存在一個實例的類(在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時看到的,在這里對兩位作者表示感謝。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容