單例模式SingleTon

單例模式

一個類只生成一個對象(確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例)

通過定義一個private的構造函數,避免被其他類new出來一個對象,不可以被實例化
這裏寫圖片描述

1.單例模式的懶加載 (第一次運行到此處,加載到內存)


class Singleton
{
private:
    static Singleton* mySingleton;
public:
    static A* getSingleton()
    {
        if(mySingleton != null)
        {
            return mySingleton;
        }
    }
};
static Singleton* mySingleton::mySingleton = mySingleton();

2.單例模式的快加載 (還沒有使用就加載)

class SingletonB
{
private:
    SingletonB();
    static SingletonB* mySingleton;
    Static pthread_mutex mutex;
public:
    static SingletonB* getSingleton()
    {
        mutex_lock();
        if(mySingleton == null)
        {
            return mySingleton;
        }
        mutex_unlock();
    }
    return mySingleton;
};
SingletonB* mySingletonB::mySingleton = null;
pthread_mutex_t SingletonB::mutex;

之前面試被問到一個問題,爲什麼要使用單例模式而不使用全局變量呢?兩者又有什麼區別呢?
當時想到的不是很多,於是後期整理了一下

單例模式和全局變量區別:

  • 全局變量可以創建多個實例,但單例模式只能創建一個(每次通過屬性Instance得到實例)
  • 全局對象創建之後在棧上保存,但單例模式哪裏第一次用到就在哪裏創建
  • 全局變量在項目中易引起變量名衝突,需要增加成本去維護,增加函數和模塊之間的耦合度
  • 當多個線程訪問全局變量,需要使用同步機制進行保護這些變量

單例模式的優點

1)由於單例模式在內存中只有一個實例,減少了內存開支,特別是一個對象需要頻繁地創建、銷燬時,而且創建或銷燬時性能又無法優化。
2)由於只生成一個實例,減少了系統的性能開銷,當一個對象的產生需要比較多的資源時,如讀取配置、產生其他依賴對象時,則可以通過在應用啓動時直接產生一個單例對象,然後用永久駐留內存的方式解決。
3)可以避免對資源的多重佔用,例如一個寫文件動作,由於只有一個實例存在中,避免對同一個資源文件的同時寫操作。
4)單例模式可以在系統設置全局的訪問點,優化和共享資源訪問。例如可以設計一個單例類,負責所有數據表的映射處理。

單例模式的缺點:

1)單例模式一般沒有接口,擴展很困難。因爲單例模式要求”自行實例化”,並且提供單一實例、接口或抽象類是不可能被實例化。特殊情況下可以實現接口、被繼承等,需要在系統開發中根據環境判斷。
2)單例模式對測試是不利的。在並行環境中,如果單例模式沒有完成,是不能進行測試的,沒有接口也不是使用mock的方式虛擬一個對象。
3)單例模式與單一職責原則有衝突。一個類應該只實現一個邏輯,而不關心他是否是單例模式,是不是單例取決於環境。

使用場景

在一個系統中,要求一個類有且僅有一個對象,如果出現多個對象就會出現“不良反應”,
具體場景如下:

1)要求生成唯一序列號的環境;
2)在整個項目中需要一個共享訪問點或共享數據,例如一個web頁面上的計數器,可以不用把每次刷新都記錄到數據庫中,使用單例模式保存計數器的值,並確保是線程安全的。
3)創建一個對象需要消耗的資源過多,如要訪問IO和數據庫等資源;
4)需要定義大量的靜態常量和靜態方法的環境,可以是單例模式(或者聲明爲static)
注意事項:
在高併發情況下,需要注意單例模式的線程同步問題。


實現Singleton模式

如果讓我們設計一個類,只能生成該類的一個實例,我們需要把構造函數設爲私有函數以禁止他人創
建實例。可以定義一個靜態的實例,在需要的時候創建該實例。  
public sealed class Singleton1
{
    private Singleton1()
    {
    }

    private static Singleton1 instance = null;
    public static Singleton1 Instance
    {
        get
        {
            if ( instance == null )
                instance = new Singleton1();
            return instance;
        }
    }
}

此次代碼實現在Singleton1的靜態屬性Instance中,只有在instance爲null時才創建一個實例,以避免重複創建。同時把構造函數定義爲私有函數,可以確保只創建一個實例。

但是,這只適用於單線程環境,在多線程的情況下就會產生問題。設想如果兩個線程同時運行到判斷instance是否爲null的if條件語句,並且此時instance沒有創建時,那麼兩個線程都會創建一個實例,此時Singleton1就不再滿足單例模式的要求了。

在《劍指offer》中,看到了可以在多線程下進行工作的單例模式。

爲了保證在多線程下只能得到類型的一個實例,需要加上一個同步鎖。

public sealed class Singleton2
{
    private Singleton2()
    {
    }

    private static readonly object syncObj = new object();

    private static Singleton2 instance = null;
    public static Singleton2 Instance
    {
        get
        {
            lock ( syncObj )
            {
                if ( instance == null )
                    instance = new Singleton2();
            }
            return instance;
        }
    }
}
我們還是假設有兩個線程同時向創建一個實例。由於在一個時刻只有一個線程能得到同步鎖,當第
一個線程加上鎖,第二個線程只能等待。當第一個線程發現實例還沒有創建時,會創建一個實例。接
着第一個線程釋放同步鎖,此時第二個線程可以加上同步鎖並運行接下來的代碼。


這時候由於實例已經被第一個線程創建出來,第二個線程就不會重複創建實例,保證了多線程下只得
到一個實例。

但是,每次通過屬性Instance得到實例,都會試圖加上一個同步鎖,而加鎖是一個非常耗時的操作,在沒有必要的時候我們應該去避免加鎖。

可行的解法:加同步鎖前後兩次判斷實例是否存在

我們只是在實例沒有創建之前需要加鎖操作,以保證只有一個線程創建出實例。而當實例已經創建之後,我們已經不需要再做枷鎖操作了。

public sealed class Singleton3
{
    private Singleton3()
    {
    }
    private static object syncObj = new object();

    private static Singleton3 instance = null;
    public static Single3 Instance
    {
        get 
        {
            if ( instance == null )
            {
                lock (instance == null )
                {
                    if (instance == null )
                        instance = new Singleton3();
                }
            }

            return instance;
        }
    }
}

Singleton3只有當instance爲null即沒有創建時,需要加鎖操作。當instance已經創建,則無需進行加鎖。這樣下來相對於Singleton2,效率好很多。

還有一種解法,利用靜態構造函數

這裏利用到了C#種一個語法特性(PS;本人對C#不是特別瞭解,也是從書中學到的一種)。
C#語法中有一個函數能確保只調用依次,那就是靜態構造函數。
實現如下:

public sealed class Singleton4
{
    private Singleton4()
    {
    }

    private static Singleton4 instance = new Singleton4();
    public static Singleton4 Instance
    {
        get
        {
            return instance;
        }
    }
}

由於C#是在調用靜態構造函數時初始化靜態變量,.NET運行時能夠確保只調用依次靜態構造函數,保證了只初始化一次instance。.NET 運行時發現第一次使用一個類型時會自動調用該類型的靜態構造函數。因此,在Singleton4中,實例instance會在第一次用到Singleton4時會被創建。

但是,這個實現單例模式方式,仍然會過早地創建實例,從而降低內存的使用效率


接下來還有非常不錯的解法:實現按需創建實例

public sealed class Singleton5
{
    Singleton5()
    {
    }

    public static Single Singleton5 Instance
    {
        get
        {
            return Nested.instance;
        }
    }

    class Nested  /*類默認私有*/
    {
        static Nested()
        {
        }

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

在Singleton5中,在內部定義一個私有類型Nested。當第一次用到這個嵌套類型時,會調用靜態構造函數創建Singleton5的實例instance。
類型Nested只在屬性Singleton5.Instance中被用到,由於其私有屬性他人無法使用Nested類型。因此當我們第一次試圖通過屬性Singleton5.Instance得到Singleton5的實例時,會自動調用Nested的靜態構造函數創建實例instance。
當然,如果不調用屬性Singleton5.Instance,也就不會觸發.NET運行時調用Nested,也不會創建實例,真正做到了按需創建。

上述提到的這些方法中,

  • Singleton1在多線程環境中不能正常工作;
  • Singleton2通過加同步鎖,雖然能在多線程下正常工作但時間效率很低;
  • Singleton3通過加同步鎖前後兩次判斷實例是否存在,確保只創建一個實例;
  • Singleton4利用了C#的靜態構造函數的特性,確保只創建一個實例;
  • Singleton5利用私有嵌套類型的特性,做到只有在真正需要時纔會創建,提高空間使用效率。

參考資料:
《設計模式之禪》
《GoF23種設計模式解析》
《劍指offer》

發佈了40 篇原創文章 · 獲贊 6 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章