1. 程式人生 > >用心理解設計模式——單例模式 (Singleton Pattern)

用心理解設計模式——單例模式 (Singleton Pattern)

前置文章: 用心理解設計模式——設計模式的原則 

設計模式相關程式碼已統一放至 我的 Github

 

一、定義

  建立型模式之一。

  Ensure a class has only one instance, and provide a global point of access to it.

(確保某一個類只有一個例項,而且自行例項化並吸納過整個系統提供這個例項)

二、要點

  將類的非靜態建構函式私有化。只在類內部例項一次。

三、評價

  只允許建立單個例項,滿足一些管理需求,節省記憶體,加快物件訪問速度等。

四、實現方式

  1.餓漢式

    顧名思義:比較飢渴、比較急,不管用不用,類載入時就先建立例項。

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

    static public Singleton GetInstance()
    {
        return instance;
    }
	
    private Singleton() { }
}

  

2.懶漢式

    顧名思義:比較懶,等到用的時候才建立例項。

public sealed class Singleton 
{
    static private Singleton instance;

    static public Singleton GetInstance()
    {
        if (instance == null) { instance = new Singleton(); }
        return instance;
    }
	
    private Singleton() { }
}

兩種基本方式比較:

  執行緒安全?:

    餓漢式,是執行緒安全的,因為,類只會載入一次,例項必然只會建立一次。

    懶漢式,是執行緒不安全的,因為,多執行緒下,如果不同執行緒同時進入 if判斷,就會生成多個例項物件。

  延遲例項化?:

    餓漢式,不管是否使用都會建立例項,如果最終沒有使用,造成記憶體浪費。

    懶漢式,實際使用時才建立例項,節省記憶體。

3.靜態初始化式 (餓漢模式在Lazy Loading上的優化(不徹底)。.Net中首選的方式)

public sealed class Singleton 
{
    static private readonly Singleton instance;

    static public Singleton GetInstance()
    {
        return instance;
    }
    
    //靜態構造
    static Singleton()
    {
        instance = new Singleton();
    }
	
    private Singleton() { }
}

  要點理解: C#靜態建構函式

  特點:它是執行緒安全的, 並且將例項化從類載入時延遲到了類首次使用時。

4.雙重檢查鎖模式 (飽漢模式在多執行緒上的優化)

public sealed class Singleton
{
    private static Singleton instance;

    private static readonly object theLock = new object ();
 
    private Singleton () { }
 
    public static Singleton GetInstance ()
    {
        // 第一重檢查, 避免 “例項已經存在時仍然執行lock獲取鎖, 影響效能” 的問題。
        if (instance == null) 
        {
            //讓執行緒排隊執行被lock包圍的程式碼段
            lock (theLock) 
            {
                //第二重檢查, 僅讓隊首的執行緒進入if建立例項。
                if (instance == null) 
                {
                    instance = new Singleton ();
                }
            }
        }
        return instance;
    }
}

   要點理解: C# 的 lock 關鍵字  。

   特點:它是執行緒安全的,並且將例項化延遲到了單例使用時。

5.靜態內部類式

public sealed class Singleton
{
    //此靜態內部類,只有在第一次使用時才會載入
    static private class Holder
    {
        static public readonly Singleton instance = new Singleton();
    }

    static public Singleton GetInstance()
    {
        return Holder.instance;
    }

    private Singleton() { }
}

  要點理解:內部類在第一次使用時才被載入,其直接在定義時初始化的靜態成員也在此時進行初始化。
  特點:它是執行緒安全的,並且將例項化延遲到了單例使用時。

6.Lazy<T>式

//注意:僅.NET4 以上可用
public sealed class Singleton
{
    static private readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());
 
    public static Singleton GetInstance 
    {
        return lazy.Value; 
    }
 
    private Singleton() { }
}

  要點理解:如何執行物件的延遲初始化

  特點:它是執行緒安全的,並且將例項化延遲到了單例使用時。適合較大單例物件。

五、繼承和複用問題

  建構函式被宣告為私有的類, 不能被繼承。

  因為子類在例項化時需要先呼叫父類的建構函式,而父類建構函式因為是私有的所以不可訪問。

  在C#中嘗試,會報錯如下圖。

  單例類的建構函式被宣告為私有,所以單例類不能被繼承。 
  通常,單例類會直接宣告為sealed,來明確表示不能被繼承。

  --------------------------------------------------------------------------------------------------------

  在實際專案中,是有可能出現大量單例類的,如果每個都要實現一遍單例似乎比較麻煩,

  利用泛型可以勉強解決這個問題。 為什麼說勉強,請看下面程式碼。

  ( 這裡以 靜態初始化式來展示,其他方式類似)。

namespace Singleton.Generic
{
    //泛型單例抽象類
    public abstract class Singleton<T> where T : class, new()
    {
        static private readonly T instance;

        static public T GetInstance()
        {
            return instance;
        }

        //靜態構造
        static Singleton()
        {
            // T要能被例項化,必須在where中新增 new() 約束,同時T必須提供一個公有的無參建構函式
            instance = new T();
        }

        //為了能被子類繼承,只能放棄父類中對單例的建構函式進行私有化這個要點
        //private Singleton() { }
    }

    //通過繼承實現實際的單例類
    public class A : Singleton<A>
    {
        //為了能夠被例項化,只能放棄子類中對單例的建構函式進行私有化這個要點
        //private A() { }
    }

    public class Client
    {
        private void Main()
        {
            //期望這樣訪問
            A instatnce = A.GetInstance();

            //實際因為沒私有化構造方法,可以在外部執行多次例項化
            A a = new A();
            A aa = new A();
            A aaa = new A();
        }
    }
}

  這樣做的缺點非常明顯,

  為了能繼承和複用,父子類中都放棄了 “對建構函式進行私有化”這個單例模式要點,實際可以在外部執行多次例項化。

  之後,我又嘗試了使用反射 (T)Activator.CreateInstance<T>() 的方式建立例項,這時如果對T的建構函式進行私有化,雖然在編譯階段不會報錯,但會在執行時報錯:MissingMethodException: Method not found: 'Default constructor not found...ctor() of A'. 可見子類要想在父類中被例項化,必須得提供至少有一個公有的建構函式。(Java中似乎可以通過反射在執行時對建構函式setAccessible,改為public進行構造)

結論:如果想要對單例模式進行繼承複用,只能公開建構函式,但是這樣違背單例模式的例項限制,不安全,不推薦使用。

六、靜態類和單例模式

兩者用起來比較相似,讓人常常很糾結到底應該選哪個。

從以下幾個方面作對比:

1.最直觀的,看類是否有成員變數,如果一個類中只是提供一些方法,沒有成員變數。那實在沒必要使用單例類,直接用靜態類即可。

否則,

2.從執行效率,靜態類執行效率更高。

3.從成員大小,如果類的職責比較龐大,並且只是在需要的時候才可能用到。就應該考慮是否需要懶載入優化記憶體,靜態類最多推遲到在靜態構造方法中初始化靜態成員變數,而單例可以推遲到實際使用時。

4.從程式設計思想,單例類面向物件,可以繼承類(注意不能被繼承,上邊說了),可以有動態多型,可以實現介面。靜態類面向過程

5.從使用簡便性,靜態類直接 類名.方法() ;單例模式要用 類名.GetInstance().方法()。