C# 單例模式(Singleton)

摘要

       在咱們平常的工做中常常須要在應用程序中保持一個惟一的實例,如:IO處理,數據庫操做等,因爲這些對象都要佔用重要的系統資源,因此咱們必須限制這些實例的建立或始終使用一個公用的實例,這就是咱們今天要介紹的——單例模式(Singleton)。html

       使用頻率clip_image001數據庫

       單件模式(Singleton):保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。設計模式

正文

singleton

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

 

       單例模式(Singleton)是幾個建立模式中最對立的一個,它的主要特色不是根據用戶程序調用生成一個新的實例,而是控制某個類型的實例惟一性,經過 上圖咱們知道它包含的角色只有一個,就是Singleton,它擁有一個私有構造函數,這確保用戶沒法經過new直接實例它。除此以外,該模式中包含一個 靜態私有成員變量instance與靜態公有方法Instance()。Instance()方法負責檢驗並實例化本身,而後存儲在靜態成員變量中,以確 保只有一個實例被建立。服務器

 

clip_image002

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

 

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

 

    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_1

  以上的實現方式適用於單線程環境,由於在多線程的環境下有可能獲得Singleton類的多個實例。假如同時有兩個線程去判斷(null == _singleton),而且獲得的結果爲真,那麼兩個線程都會建立類Singleton的實例,這樣就違背了Singleton模式「惟一實例」的初衷。dom

版本二線程安全

/// <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());
            }
        }
    }
}
Singleton_2

  以上方式的實現方式是線程安全的,首先咱們建立了一個靜態只讀的進程輔助對象,因爲lock是確保當一個線程位於代碼的臨界區時,另外一個線程不能進入臨界 區(同步操做)。若是其餘線程試圖進入鎖定的代碼,則它將一直等待,直到該對象被釋放。從而確保在多線程下不會建立多個對象實例了。只是這種實現方式要進 行同步操做,這將是影響系統性能的瓶頸和增長了額外的開銷。ide

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

  在介紹第四種實現方式以前,首先讓咱們認識什麼是,當字段被標記爲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;
        }
    }
}
Singleton_4

  以上方式實現比以前介紹的方式都要簡單,但它確實是多線程環境下,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();
    }
}
Singleton_5

  這裏咱們把初始化工做放到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()
    {
    }
}
Singleton_6

  這種方式的簡單和性能良好,並且還提供檢查是否已經建立實例的屬性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輸出結果

總結

單例模式的優勢:

單例模式(Singleton)會控制其實例對象的數量,從而確保訪問對象的惟一性。

  1. 實例控制:單例模式防止其它對象對本身的實例化,確保全部的對象都訪問一個實例。
  2. 伸縮性:由於由類本身來控制實例化進程,類就在改變實例化進程上有相應的伸縮性。

 

單例模式的缺點:

  1. 系統開銷。雖然這個系統開銷看起來很小,可是每次引用這個類實例的時候都要進行實例是否存在的檢查。這個問題能夠經過靜態實例來解決。
  2. 開發混淆。當使用一個單例模式的對象的時候(特別是定義在類庫中的),開發人員必需要記住不能使用new關鍵字來實例化對象。由於開發者看不到在類庫中的源代碼,因此當他們發現不能實例化一個類的時候會很驚訝。
  3. 對象生命週期。單例模式沒有提出對象的銷燬。在提供內存管理的開發語言(好比,基於.NetFramework的語言)中,只有單例模式對象 本身才能將對象實例銷燬,由於只有它擁有對實例的引用。在各類開發語言中,好比C++,其它類能夠銷燬對象實例,可是這麼作將致使單例類內部的指針指向不 明。

 

單例適用性

使用Singleton模式有一個必要條件:在一個系統要求一個類只有一個實例時才應當使用單例模式。反之,若是一個類能夠有幾個實例共存,就不要使用單例模式。

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

不要將數據庫鏈接作成單例,由於一個系統可能會與數據庫有多個鏈接,而且在有鏈接池的狀況下,應當儘量及時釋放鏈接。Singleton模式因爲使用靜態成員存儲類實例,因此可能會形成資源沒法及時釋放,帶來問題。

 

 

========================================================

單例模式(Singleton)

C#設計模式學習筆記-單例模式

C#設計模式(1)——單例模式

相關文章
相關標籤/搜索