一個單例還能寫出花來?


什么是單例模式?

從“單例”字面意思上理解為——一個類只有一個實例,所以單例模式也就是保證一個類只有一個實例的一種實現方法罷了。
其官方定義為:確保一個類只有一個實例,並提供一個全局訪問點

為什么會有單例模式?

從單例模式的定義中我們可以看出——單例模式的使用自然是當我們的系統中某個對象只需要一個實例的情況。

剖析單例模式實現思路

  1. 明確目的:(1)確保一個類只有一個實例;(2)提供一個訪問它的全局訪問點;
  2. 類的實例化基本都是通過關鍵字new的,而定義私有的構造函數就不能在外界通過new創建實例,類實例的創建在類里面;
  3. 每個線程都有自己的線程棧,定義一個靜態私有變量保存類的實例主要是為了在多線程確保類有一個實例;
  4. 定義一個公有靜態方法是為了公開類的實例;

簡單代碼實現如下:

/// <summary>
    /// 單例模式(確保一個類只有一個實例,並提供一個全局訪問點)
    /// </summary>
    public sealed class Singleton
    {
	    /// <summary>
	    /// 私有靜態變量保存類的唯一實例
	    /// </summary>
	    private static Singleton uniqueInstance;
	    
	    /// <summary>
	    /// 私有構造方法,避免外部 new
	    /// </summary>
	    private Singleton() { }
	    
	    /// <summary>
	    /// 暴露全局訪問點
	    /// </summary>
	    /// <returns></returns>
	    public static Singleton GetInstance()
	    {
		    if (uniqueInstance == null)
		    uniqueInstance = new Singleton();
		    
		    return uniqueInstance;
	    }
    }

上面的單例模式的實現在單線程下確實是完美的,然而在多線程的情況下會得到多個Singleton實例,因為在兩個線程同時運行GetInstance方法時,此時兩個線程判斷(uniqueInstance ==null)這個條件時都返回真,此時兩個線程就都會創建Singleton的實例,這樣就違背了我們單例模式初衷了,既然上面的實現會運行多個線程執行,那我們對於多線程的解決方案自然就是使GetInstance方法在同一時間只運行一個線程運行就好了,也就是我們線程同步的問題了,具體的解決多線程的代碼如下:

/// <summary>
/// 單例模式(確保一個類只有一個實例,並提供一個全局訪問點,線程同步)
/// </summary>
public sealed class Singleton_MultiThread
{
    /// <summary>
    /// 私有靜態變量保存類的唯一實例
    /// </summary>
    private static Singleton_MultiThread uniqueInstance;

    /// <summary>
    /// 鎖,確保線程同步
    /// </summary>
    private static readonly object locker = new object();

    /// <summary>
    /// 私有構造方法,避免外部 new
    /// </summary>
    private Singleton_MultiThread() { }

    /// <summary>
    /// 暴露全局訪問點
    /// </summary>
    /// <returns></returns>
    public static Singleton_MultiThread GetInstance1()
    {
        // 當第一個線程運行到這里時,此時會對locker對象 "加鎖",
        // 當第二個線程運行該方法時,首先檢測到locker對象為"加鎖"狀態,該線程就會掛起等待第一個線程解鎖
        // lock語句運行完之后(即線程運行完之后)會對該對象"解鎖"
        lock (locker)
        {
            if (uniqueInstance == null)
                uniqueInstance = new Singleton_MultiThread();
        }

        return uniqueInstance;
    }

    /// <summary>
    /// 暴露全局訪問點(雙重鎖定,減小開銷,提升性能)
    /// </summary>
    /// <returns></returns>
    public static Singleton_MultiThread GetInstance2()
    {
        // 當第一個線程運行到這里時,此時會對locker對象 "加鎖",
        // 當第二個線程運行該方法時,首先檢測到locker對象為"加鎖"狀態,該線程就會掛起等待第一個線程解鎖
        // lock語句運行完之后(即線程運行完之后)會對該對象"解鎖"
        // 雙重鎖定只需要加一句判斷就可以了
        if (uniqueInstance == null)
        {
            lock (locker)
            {
                if (uniqueInstance == null)
                    uniqueInstance = new Singleton_MultiThread();
            }
        }

        return uniqueInstance;
    }
}

上面這種解決方案確實可以解決多線程的問題,但是上面GetInstance1()對於每個線程都會對線程輔助對象locker加鎖之后再判斷實例是否存在,對於這個操作完全沒有必要的,因為當第一個線程創建了該類的實例之后,后面的線程此時只需要直接判斷(uniqueInstance==null)為假,此時完全沒必要對線程輔助對象加鎖之后再去判斷,所以上面的實現方式增加了額外的開銷,損失了性能,為了改進上面實現方式的缺陷,我們只需要在lock語句前面加一句(uniqueInstance==null)的判斷就可以避免鎖所增加的額外開銷,這種實現方式我們就叫它 “雙重鎖定”,可參考GetInstance2()代碼。

單例模式的其他實現方法

靜態初始化
public sealed class Singleton_StaticInit
{
    private static readonly Singleton_StaticInit _instance = new Singleton_StaticInit();

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

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

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

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

延遲初始化
public sealed class Singleton_LazyInit
{
    private Singleton_LazyInit()
    {
    }

    /// <summary>
    /// Gets the instance.
    /// </summary>
    public static Singleton_LazyInit 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_LazyInit _instance = new Singleton_LazyInit();
    }
}

這里我們把初始化工作放到Nested類中的一個靜態成員來完成,這樣就實現了延遲初始化。上面了一個嵌套類借鑒了.Net中lambda和匿名函數的實現原理。

Lazy< T > type
/// <summary>
/// .NET 4's Lazy<T> type
/// </summary>
public sealed class Singleton_LazyType
{
    private static readonly Lazy<Singleton_LazyType> lazy =
        new Lazy<Singleton_LazyType>(() => new Singleton_LazyType());

    public static Singleton_LazyType Instance { get { return lazy.Value; } }

    private Singleton_LazyType()
    {
    }
}

這種方式的簡單和性能良好,而且還提供檢查是否已經創建實例的屬性IsValueCreated。

單例模式總結

單例模式的優點:

單例模式(Singleton)會控制其實例對象的數量,從而確保訪問對象的唯一性。
實例控制:單例模式防止其它對象對自己的實例化,確保所有的對象都訪問一個實例。
伸縮性:因為由類自己來控制實例化進程,類就在改變實例化進程上有相應的伸縮性。

單例模式的缺點:

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

單例模式的適用性:

使用Singleton模式有一個必要條件:在一個系統要求一個類只有一個實例時才應當使用單例模式。反之,如果一個類可以有幾個實例共存,就不要使用單例模式。
不要使用單例模式存取全局變量。這違背了單例模式的用意,最好放到對應類的靜態成員中。
不要將數據庫連接做成單例,因為一個系統可能會與數據庫有多個連接,並且在有連接池的情況下,應當盡可能及時釋放連接。Singleton模式由於使用靜態成員存儲類實例,所以可能會造成資源無法及時釋放,帶來問題。

參考資料

http://www.cnblogs.com/rush/archive/2011/10/30/2229565.html
http://csharpindepth.com/Articles/General/Singleton.aspx

https://github.com/SnailDev/SnailDev.DesignPattern


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM