1. 程式人生 > >單例模式(Singleton)的6種實現

單例模式(Singleton)的6種實現

1.1.1 摘要

       在我們日常的工作中經常需要在應用程式中保持一個唯一的例項,如:IO處理,資料庫操作等,由於這些物件都要佔用重要的系統資源,所以我們必須限制這些例項的建立或始終使用一個公用的例項,這就是我們今天要介紹的——單例模式(Singleton)。

       使用頻率clip_image001 高

       單件模式(Singleton):保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。

1.1.2 正文

singleton

圖1單例模式(Singleton)結構圖

       單例模式(Singleton)是幾個建立模式中最對立的一個,它的主要特點不是根據使用者程式呼叫生成一個新的例項,而是控制某個型別的例項唯一性,通過上圖我們知道它包含的角色只有一個,就是Singleton,它擁有一個私有建構函式,這確保使用者無法通過new直接例項它。除此之外,該模式中包含一個靜態私有成員變數instance與靜態公有方法Instance()。Instance()方法負責檢驗並例項化自己,然後儲存在靜態成員變數中,以確保只有一個例項被建立。

clip_image002

圖2單例模式(Singleton)邏輯模型

       接下來我們將介紹6中不同的單例模式(Singleton)的實現方式。這些實現方式都有以下的共同點:

  1. 有一個私有的無參建構函式,這可以防止其他類例項化它,而且單例類也不應該被繼承,如果單例類允許繼承那麼每個子類都可以建立例項,這就違背了Singleton模式“唯一例項”的初衷。
  2. 單例類被定義為sealed,就像前面提到的該類不應該被繼承,所以為了保險起見可以把該類定義成不允許派生,但沒有要求一定要這樣定義。
  3. 一個靜態的變數用來儲存單例項的引用。
  4. 一個公有的靜態方法用來獲取單例項的引用,如果例項為null即建立一個。

版本一執行緒不安全

/// <summary>
/// A simple singleton class implements.
/// </summary>
public sealed class Singleton
{
    private static Singleton _instance = null;

    /// <summary>
    /// Prevents a default instance of the 
    /// <see cref="Singleton"/> class from being created.
    /// </summary>
    
private Singleton() { } /// <summary> /// Gets the instance. /// </summary> public static Singleton Instance { get { return _instance ?? (_instance = new Singleton()); } } }

      以上的實現方式適用於單執行緒環境,因為在多執行緒的環境下有可能得到Singleton類的多個例項。假如同時有兩個執行緒去判斷

(null == _singleton),並且得到的結果為真,那麼兩個執行緒都會建立類Singleton的例項,這樣就違背了Singleton模式“唯一例項”的初衷。

版本二執行緒安全

/// <summary>
/// A thread-safe singleton class.
/// </summary>
public sealed class Singleton
{
    private static Singleton _instance = null;
    private static readonly object SynObject = new object();

    Singleton()
    {
    }

    /// <summary>
    /// Gets the instance.
    /// </summary>
    public static Singleton Instance
    {
        get
        {
            // Syn operation.
            lock (SynObject)
            {
                return _instance ?? (_instance = new Singleton());
            }
        }
    }
}

        以上方式的實現方式是執行緒安全的,首先我們建立了一個靜態只讀的程序輔助物件,由於lock是確保當一個執行緒位於程式碼的臨界區時,另一個執行緒不能進入臨界區(同步操作)。如果其他執行緒試圖進入鎖定的程式碼,則它將一直等待,直到該物件被釋放。從而確保在多執行緒下不會建立多個物件例項了。只是這種實現方式要進行同步操作,這將是影響系統性能的瓶頸和增加了額外的開銷。

Double-Checked Locking

       前面講到的執行緒安全的實現方式的問題是要進行同步操作,那麼我們是否可以降低通過操作的次數呢?其實我們只需在同步操作之前,新增判斷該例項是否為null就可以降低通過操作的次數了,這樣是經典的Double-Checked Locking方法。

/// <summary>
/// Double-Checked Locking implements a thread-safe singleton class
/// </summary>
public sealed class Singleton
{
    private static Singleton _instance = null;
    // Creates an syn object.
    private static readonly object SynObject = new object();

    Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            // Double-Checked Locking
            if (null == _instance)
            {
                lock (SynObject)
                {
                    if (null == _instance)
                    {
                        _instance = new Singleton();
                    }
                }
            }
            return _instance;
        }
    }
}

       在介紹第四種實現方式之前,首先讓我們認識什麼是,當欄位被標記為beforefieldinit型別時,該欄位初始化可以發生在任何時候任何欄位被引用之前。這句話聽起了有點彆扭,接下來讓我們通過具體的例子介紹。

/// <summary>
/// Defines a test class.
/// </summary>
class Test
{
    public static string x = EchoAndReturn("In type initializer");

    public static string EchoAndReturn(string s)
    {
        Console.WriteLine(s);
        return s;
    }
}

      上面我們定義了一個包含靜態欄位和方法的類Test,但要注意我們並沒有定義靜態的建構函式。

singleton2

圖3 Test類的IL程式碼

class Test
{
    public static string x = EchoAndReturn("In type initializer");

    // Defines a parameterless constructor.
    static Test()
    {
    }

    public static string EchoAndReturn(string s)
    {
        Console.WriteLine(s);
        return s;
    }
}

    上面我們給Test類新增一個靜態的建構函式。

  singleton3

圖4 Test類的IL程式碼

       通過上面Test類的IL程式碼的區別我們發現,當Test類包含靜態欄位,而且沒有定義靜態的建構函式時,該類會被標記為beforefieldinit。

       現在也許有人會問:“被標記為beforefieldinit和沒有標記的有什麼區別呢”?OK現在讓我們通過下面的具體例子看一下它們的區別吧!

class Test
{
    public static string x = EchoAndReturn("In type initializer");

    static Test()
    {
    }

    public static string EchoAndReturn(string s)
    {
        Console.WriteLine(s);
        return s;
    }
}

class Driver
{
    public static void Main()
    {
        Console.WriteLine("Starting Main");
        // Invoke a static method on Test
        Test.EchoAndReturn("Echo!");
        Console.WriteLine("After echo");
        Console.ReadLine();

        // The output result:
        // Starting Main
        // In type initializer
        // Echo!
        // After echo            
    }
}

     我相信大家都可以得到答案,如果在呼叫EchoAndReturn()方法之前,需要完成靜態成員的初始化,所以最終的輸出結果如下:

singleton4

圖5輸出結果

    接著我們在Main()方法中新增string y = Test.x,如下:

public static void Main()
{
    Console.WriteLine("Starting Main");
    // Invoke a static method on Test
    Test.EchoAndReturn("Echo!");
    Console.WriteLine("After echo");

    //Reference a static field in Test
    string y = Test.x;
    //Use the value just to avoid compiler cleverness
    if (y != null)
    {
        Console.WriteLine("After field access");
    }
    Console.ReadKey();

    // The output result:
    // In type initializer
    // Starting Main
    // Echo!
    // After echo
    // After field access

}

singleton5

圖6 輸出結果

        通過上面的輸出結果,大家可以發現靜態欄位的初始化跑到了靜態方法呼叫之前,Wo難以想象啊!

        最後我們在Test類中新增一個靜態建構函式如下:

class Test
{
    public static string x = EchoAndReturn("In type initializer");

    static Test()
    {
    }

    public static string EchoAndReturn(string s)
    {
        Console.WriteLine(s);
        return s;
    }
}

singleton6

圖7 輸出結果

       理論上,type initializer應該發生在”Echo!”之後和”After echo”之前,但這裡卻出現了不唯一的結果,只有當Test類包含靜態建構函式時,才能確保type initializer的初始化發生在”Echo!”之後和”After echo”之前。

所以說要確保type initializer發生在被欄位引用時,我們應該給該類新增靜態建構函式。接下來讓我們介紹單例模式的靜態方式。

靜態初始化

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

    // Explicit static constructor to tell C# compiler
    // not to mark type as beforefieldinit
    static Singleton()
    {
    }

    /// <summary>
    /// Prevents a default instance of the 
    /// <see cref="Singleton"/> class from being created.
    /// </summary>
    private Singleton()
    {
    }

    /// <summary>
    /// Gets the instance.
    /// </summary>
    public static Singleton Instance
    {
        get
        {
            return _instance;
        }
    }
}

        以上方式實現比之前介紹的方式都要簡單,但它確實是多執行緒環境下,C#實現的Singleton的一種方式。由於這種靜態初始化的方式是在自己的欄位被引用時才會例項化。

       讓我們通過IL程式碼來分析靜態初始化。

singleton7

圖8靜態初始化IL程式碼

        首先這裡沒有beforefieldinit的修飾符,由於我們添加了靜態建構函式當靜態欄位被引用時才進行初始化,因此即便很多執行緒試圖引用_instance,也需要等靜態建構函式執行完並把靜態成員_instance例項化之後可以使用。

延遲初始化

/// <summary>
/// Delaies initialization.
/// </summary>
public sealed class Singleton
{
    private Singleton()
    {
    }

    /// <summary>
    /// Gets the instance.
    /// </summary>
    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();
    }
}

   這裡我們把初始化工作放到Nested類中的一個靜態成員來完成,這樣就實現了延遲初始化。

Lazy<T> type

/// <summary>
/// .NET 4's Lazy<T> type
/// </summary>
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。

具體例子

     現在讓我們使用單例模式(Singleton)實現負載平衡器,首先我們定義一個伺服器類,它包含伺服器名和IP地址如下:

/// <summary>
/// Represents a server machine
/// </summary>
class Server
{
    // Gets or sets server name
    public string Name { get; set; }

    // Gets or sets server IP address
    public string IP { get; set; }
}

     由於負載平衡器只提供一個物件例項供伺服器使用,所以我們使用單例模式(Singleton)實現該負載平衡器。

/// <summary>
/// The 'Singleton' class
/// </summary>
sealed class LoadBalancer
{
    private static readonly LoadBalancer _instance =
        new LoadBalancer();

    // Type-safe generic list of servers
    private List<Server> _servers;
    private Random _random = new Random();

    static LoadBalancer()
    {
    }

    // Note: constructor is 'private'
    private LoadBalancer()
    {
        // Load list of available servers
        _servers = new List<Server> 
            { 
              new Server{ Name = "ServerI", IP = "192.168.0.108" },
              new Server{ Name = "ServerII", IP = "192.168.0.109" },
              new Server{ Name = "ServerIII", IP = "192.168.0.110" },
              new Server{ Name = "ServerIV", IP = "192.168.0.111" },
              new Server{ Name = "ServerV", IP = "192.168.0.112" },
            };
    }

    /// <summary>
    /// Gets the instance through static initialization.
    /// </summary>
    public static LoadBalancer Instance
    {
        get { return _instance; }
    }


    // Simple, but effective load balancer
    public Server NextServer
    {
        get
        {
            int r = _random.Next(_servers.Count);
            return _servers[r];
        }
    }
}

  上面負載平衡器類LoadBalancer我們使用靜態初始化方式實現單例模式(Singleton)。

static void Main()
{
    LoadBalancer b1 = LoadBalancer.Instance;
    b1.GetHashCode();
    LoadBalancer b2 = LoadBalancer.Instance;
    LoadBalancer b3 = LoadBalancer.Instance;
    LoadBalancer b4 = LoadBalancer.Instance;

    // Confirm these are the same instance
    if (b1 == b2 && b2 == b3 && b3 == b4)
    {
        Console.WriteLine("Same instance\n");
    }

    // Next, load balance 15 requests for a server
    LoadBalancer balancer = LoadBalancer.Instance;
    for (int i = 0; i < 15; i++)
    {
        string serverName = balancer.NextServer.Name;
        Console.WriteLine("Dispatch request to: " + serverName);
    }

    Console.ReadKey();
}

clip_image002[9]

圖9 LoadBalancer輸出結果

1.1.3 總結

單例模式的優點:

單例模式(Singleton)會控制其例項物件的數量,從而確保訪問物件的唯一性。

  1. 例項控制:單例模式防止其它物件對自己的例項化,確保所有的物件都訪問一個例項。
  2. 伸縮性:因為由類自己來控制例項化程序,類就在改變例項化程序上有相應的伸縮性。

單例模式的缺點:

  1. 系統開銷。雖然這個系統開銷看起來很小,但是每次引用這個類例項的時候都要進行例項是否存在的檢查。這個問題可以通過靜態例項來解決。
  2. 開發混淆。當使用一個單例模式的物件的時候(特別是定義在類庫中的),開發人員必須要記住不能使用new關鍵字來例項化物件。因為開發者看不到在類庫中的原始碼,所以當他們發現不能例項化一個類的時候會很驚訝。
  3. 物件生命週期。單例模式沒有提出物件的銷燬。在提供記憶體管理的開發語言(比如,基於.NetFramework的語言)中,只有單例模式物件自己才能將物件例項銷燬,因為只有它擁有對例項的引用。在各種開發語言中,比如C++,其它類可以銷燬物件例項,但是這麼做將導致單例類內部的指標指向不明。

單例適用性

使用Singleton模式有一個必要條件:在一個系統要求一個類只有一個例項時才應當使用單例模式。反之,如果一個類可以有幾個例項共存,就不要使用單例模式。

不要使用單例模式存取全域性變數。這違背了單例模式的用意,最好放到對應類的靜態成員中。

不要將資料庫連線做成單例,因為一個系統可能會與資料庫有多個連線,並且在有連線池的情況下,應當儘可能及時釋放連線。Singleton模式由於使用靜態成員儲存類例項,所以可能會造成資源無法及時釋放,帶來問題。

參考: