【轉】C#設計模式-單例模式(Singleton Pattern)

目錄
  • 介紹
  • 第一個版本 ——不是線程安全的
  • 第二個版本 —— 簡單的線程安全
  • 第三個版本 - 使用雙重檢查鎖定嘗試線程安全
  • 第四個版本 - 不太懶,不使用鎖且線程安全
  • 第五版 - 徹底懶惰的實例化
  • 第六版 - 使用.NET 4的 Lazy 類型
  • 性能與懶惰
  • 異常
  • 結論

介紹

單例模式是軟件工程中最着名的模式之一。從本質上講,單例是一個只容許建立自身的單個實例的類,而且一般能夠簡單地訪問該實例。最多見的是,單例不容許在建立實例時指定任何參數——不然對實例的第二個請求但具備不一樣的參數可能會有問題!(若是對於具備相同參數的全部請求都應訪問相同的實例,則工廠模式更合適。)本文僅處理不須要參數的狀況。一般,單例的要求是它們是懶惰地建立的——即直到第一次須要時才建立實例。html

在C#中實現單例模式有各類不一樣的方法。我將以優雅的相反順序呈現它們,從最多見的、不是線程安全的版本開始,一直到徹底延遲加載的、線程安全的、簡單且高性能的版本。程序員

然而,全部這些實現都有四個共同特徵:算法

  • 單個構造函數,它是私有且無參數的。這能夠防止其餘類實例化它(這將違反模式)。請注意,它還能夠防止子類化——若是一個單例對象能夠被子類化一次,那麼它就能夠被子類化兩次,若是每一個子類均可以建立一個實例,則違反了該模式。若是您須要基類型的單個實例,則可使用工廠模式,可是確切的類型要到運行時才能知道。
  • 類是密封的。嚴格來講,因爲上述緣由,這是沒必要要的,可是能夠幫助JIT進行更多的優化。
  • 一個靜態變量,用於保存對單個已建立實例的引用(若是有的話)。
  • 公共靜態意味着獲取對單個已建立實例的引用,必要時建立一個實例。

請注意,全部這些實現還使用公共靜態屬性Instance 做爲訪問實例的方法。在全部狀況下,能夠輕鬆地將屬性轉換爲方法,而不會影響線程安全性或性能。安全

第一個版本 ——不是線程安全的

// 糟糕的代碼!不要使用!
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)並發現它爲true,而後兩個都建立實例,這違反了單例模式。請注意,實際上,在計算表達式以前可能已經建立了實例,可是內存模型不保證其餘線程能夠看到實例的新值,除非已經傳遞了合適的內存屏障(互斥鎖)。併發

第二個版本 —— 簡單的線程安全

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;
            }
        }
    }
}

此實現是線程安全的。線程取消對共享對象的鎖定,而後在建立實例以前檢查是否已建立實例。這會解決內存屏障問題(由於鎖定確保在獲取鎖以後邏輯上發生全部讀取,而且解鎖確保在鎖定釋放以前邏輯上發生全部寫入)並確保只有一個線程將建立實例(僅限於一次只能有一個線程能夠在代碼的那一部分中——當第二個線程進入它時,第一個線程將建立實例,所以表達式將計算爲false)。不幸的是,每次請求實例時都會得到鎖定,所以性能會受到影響。框架

請注意,我沒有像這個實現的某些版本那樣鎖定typeof(Singleton),而是鎖定了類私有的靜態變量的值。鎖定其餘類能夠訪問和鎖定的對象(例如類型)會致使性能問題甚至死鎖。這是個人風格偏好——只要有可能,只鎖定專門爲鎖定目的而建立的對象,或者爲了特定目的(例如,等待/觸發隊列)而鎖定的文檔。一般這些對象應該是它們所使用的類的私有對象。這有助於使編寫線程安全的應用程序變得更加容易。函數

第三個版本 - 使用雙重檢查鎖定嘗試線程安全

// 糟糕的代碼!不要使用!
public sealed class 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;
        }
    }
}

該實現嘗試是線程安全的,而沒必要每次都取出鎖。不幸的是,該模式有四個缺點:性能

  • 它在Java中不起做用。這彷佛是一個奇怪的事情,可是若是您須要Java中的單例模式,這是值得知道的,C#程序員也多是Java程序員。Java內存模型沒法確保構造函數在將新對象的引用分配給Instance以前完成。Java內存模型經歷了1.5版本的從新改進,可是在沒有volatile變量(如在C#中)的狀況下,雙重檢查鎖定仍然會被破壞。
  • 在沒有任何內存障礙的狀況下,ECMA CLI規範也打破了這一限制。有可能在.NET 2.0內存模型(比ECMA規範更強)下它是安全的,但我寧願不依賴那些更強大的語義,特別是若是對安全性有任何疑問的話。使instance變量volatile變得有效,就像明確的內存屏障調用同樣,儘管在後一種狀況下,甚至專家也沒法準確地就須要哪些屏障達成一致。我儘可能避免專家對對錯意見也不一致的狀況!
  • 這很容易出錯。該模式須要徹底如上所述——任何重大變化均可能影響性能或正確性。
  • 它的性能仍然不如後續的實現。

第四個版本 - 不太懶,不使用鎖且線程安全

public sealed class Singleton
{
    private static readonly Singleton instance = new Singleton();

    // 顯式靜態構造函數告訴C#編譯器
    // 不要將類型標記爲BeforeFieldInit
    static Singleton()
    {
    }

    private Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            return instance;
        }
    }
}

正如您所看到的,這實際上很是簡單——可是爲何它是線程安全的,它有多懶惰?C#中的靜態構造函數僅在建立類的實例或引用靜態成員時執行,而且每一個AppDomain只執行一次。考慮到不管發生什麼狀況,都須要執行對新構造的類型的檢查,這比在前面的示例中添加額外檢查要快。然而,還有一些小缺陷:測試

  • 它並不像其餘實現那樣懶惰。特別是,若是您有Instance以外的靜態成員,那麼對這些成員的第一次引用將涉及到建立實例。這將在下一個實現中獲得糾正。優化

  • 若是一個靜態構造函數調用另外一個靜態構造函數,而另外一個靜態構造函數再次調用第一個構造函數,則會出現複雜狀況。查看.NET規範(目前是分區II的第9.5.3節),瞭解有關類型初始化器的確切性質的更多詳細信息——它們不太可能會影響您,可是有必要了解靜態構造函數在循環中相互引用的後果。
  • 類型初始化器的懶惰性只有在.NET沒有使用名爲BeforeFieldInit的特殊標誌標記類型時才能獲得保證。不幸的是,C#編譯器(至少在.NET 1.1運行時中提供)將全部沒有靜態構造函數的類型(即看起來像構造函數但被標記爲靜態的塊)標記爲BeforeFieldInit。我如今有一篇文章,詳細介紹了這個問題。另請注意,它會影響性能,如在頁面底部所述的那樣。

您可使用此實現(而且只有這一個)的一個快捷方式是將 Instance做爲一個公共靜態只讀變量,並徹底刪除該屬性。這使得基本的框架代碼很是小!然而,許多人更願意擁有一個屬性,以防未來須要採起進一步行動,而JIT內聯可能會使性能相同。(注意,若是您須要懶惰的,靜態構造函數自己仍然是必需的。)

第五版 - 徹底懶惰的實例化

public sealed class Singleton
{
    private Singleton()
    {
    }

    public static Singleton Instance { get { return Nested.instance; } }
        
    private class Nested
    {
        // 顯式靜態構造告訴C#編譯器
        // 未標記類型BeforeFieldInit
        static Nested()
        {
        }

        internal static readonly Singleton instance = new Singleton();
    }
}

在這裏,實例化是由對嵌套類的靜態成員的第一次引用觸發的,該引用只發生在Instance中。這意味着實現是徹底懶惰的,可是具備前面實現的全部性能優點。請注意,儘管嵌套類能夠訪問封閉類的私有成員,但反之則否則,所以須要instance在此處爲內部成員。不過,這不會引發任何其餘問題,由於類自己是私有的。可是,爲了使實例化變得懶惰,代碼要稍微複雜一些。

第六版 - 使用.NET 4的 Lazy 類型

若是您使用的是.NET 4(或更高版本),則可使用 System.Lazy 類型使惰性變得很是簡單。您須要作的就是將委託傳遞給調用Singleton構造函數的構造函數——使用lambda表達式最容易作到這一點。

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()
    {
    }
}

它很簡單,並且性能很好。它還容許您檢查是否已使用IsValueCreated 屬性建立實例(若是須要的話)。

上面的代碼隱式地將LazyThreadSafetyMode.ExecutionAndPublication用做Lazy<Singleton>的線程安全模式。根據您的要求,您可能但願嘗試其餘模式。

性能與懶惰

在許多狀況下,您實際上並不須要徹底懶惰——除非您的類初始化作了一些特別耗時的事情,或者在其餘地方產生了一些反作用,不然最好忽略上面所示的顯式靜態構造函數。這能夠提升性能,由於它容許JIT編譯器進行一次檢查(例如在方法的開頭)以確保類型已經初始化,而後從那時開始設定它。若是在相對緊密的循環中引用單例實例,則會產生(相對)顯著的性能差別。您應該決定是否須要徹底延遲實例化,並在類中適當地記錄此決策。

這個頁面存在的不少緣由是人們試圖變得聰明,所以提出了雙重檢查鎖定算法。咱們經常認爲鎖定是昂貴的,這被誤導的。我寫了一個很是快速的基準測試,在一個循環中獲取10億次單例實例,並嘗試不一樣的變體。這並非很科學,由於在現實生活中,您可能想知道若是每次迭代都涉及到對獲取單例的方法的調用,那麼速度有多快。然而這確實顯示了一個重要的觀點。在個人筆記本電腦上,最慢的解決方案(大約5倍)是鎖定解決方案(解決方案2)。這很重要嗎?可能不會,當您記住它仍然可以在40秒內獲取10億次Singleton。(注意:這篇文章最初是在好久之前寫的——如今我但願有更好的性能。)這意味着,若是你是「僅僅」每秒得到40萬次單例實例,那麼花費的成本將是1%的性能——因此不會作不少事情去改進它。如今,若是你常常 得到單例實例——你是否可能在循環中使用它?若是您很是關心如何提升性能,爲何不在循環外聲明一個局部變量,先獲取一次Singleton,而後再循環呢。Bingo,即便是最慢的實現性能也足夠了。

我很是有興趣看到一個真實的應用程序,在這個應用程序中,使用簡單鎖定和使用一種更快的解決方案之間的差別實際上會帶來顯著的性能差別。

異常

有時,您須要在單例構造函數中執行一些操做,這可能會拋出異常,但可能不會對整個應用程序形成致命影響。您的應用程序可能可以解決此問題,並但願再次嘗試。在這個階段,使用類型初始化器來構造單例會出現問題。不一樣的運行時處理這種狀況的方式不一樣,但我不知道有哪些運行時執行了所需的操做(再次運行類型初始化程序),即便有一個運行時這樣作,您的代碼也會在其餘運行時被破壞。爲了不這些問題,我建議使用文章裏列出的第二種模式 ——只需使用一個簡單的鎖,並每次都進行檢查,若是還沒有成功構建實例,則在方法/屬性中構建實例。

結論

在C#中實現單例模式有各類不一樣的方法。讀者已經寫信給我詳細說明了他已經封裝了同步方面的方法,雖然我認可這可能在一些很是特殊的狀況下有用(特別是在你想要很是高性能的地方,以及肯定單例是否已經建立,並徹底懶惰,而不考慮其餘靜態成員被調用)。我我的並不認爲這種狀況會常常出現,值得在這篇文章中進一步改進,但若是你處於這種狀況,請發郵件給我

個人我的的偏好是解決方案4:我一般惟一一次不採用它是由於我須要可以在不觸發初始化的狀況下調用其餘靜態方法,或者若是我須要知道單例是否已經被實例化。我不記得上次我處於那種狀況是何時了,假設我有過,在那種狀況下,我可能會選擇解決方案2,這仍然是很好的,很容易正確實現。

解決方案5很優雅,可是比2或4更復雜,正如我上面所說,它提供的好處彷佛只是不多有用。解決方案6是一種更簡單的方法來實現懶惰,若是你使用.NET 4.它還有一個優點,它顯然是懶惰的。我目前仍然傾向於使用解決方案4,這僅僅是出於習慣——但若是我與沒有經驗的開發人員合做,我極可能會選擇解決方案6做爲一種簡單且廣泛適用的模式。

(我不會使用解決方案1,由於它是有缺陷的,我也不會使用解決方案3,由於它的好處沒有超過5。)

 

轉載地址:http://www.javashuo.com/article/p-akrpvkmz-nv.html

原文做者:Jon Skeet

原文地址:Implementing the Singleton Pattern in C#

相關文章
相關標籤/搜索