單例模式(經典)

一 、定義

衆多設計模式中,單例模式是一種比較常見模式。本文以一個C++開發者的角度分析單例模式的幾種經典實現。

GOF定義單例模式需滿足以下兩個條件:

1、保證一個類只創建一個實例。

2、提供對實例的全局訪問點。

二、應用場景

日誌類、配置類、管理類、共享資源類

三、單例的幾種實現

3.1 lazy singleton


//頭文件中
class Singleton
{
public:
    static Singleton& Instance()
    {
        if (instance_ == NULL)
        {
            instance_ = new Singleton;
        }
        return *instance_;
    }
private:
    Singleton();
        ~Singleton();
        Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
private:
    static Singleton* instance_;
};
//實現文件中
Singleton* Singleton::instance_ = 0;

優點:這裏Instance()返回的實例的引用而不是指針,如果返回的是指針可能會有被外部調用者delete掉的隱患,所以這裏返回引用會更加保險一些。並且直到Instance()被訪問,纔會生成實例,這種特性被稱爲延遲初始化(Lazy initialization),這在一些初始化時消耗較大的情況有很大優勢。


缺點:Lazy Singleton不是線程安全的,比如現在有線程A和線程B,都通過instance_ == NULL的判斷,那麼線程A和B都會創建新實例。單例模式保證生成唯一實例的規則被打破了。


3.2 Eager Singleton

這種實現在程序開始(靜態屬性instance初始化)的時就完成了實例的創建。這正好和上述的Lazy Singleton相反。

//頭文件中
class Singleton
{
public:
    static Singleton& Instance()
    {
        return instance;
    }
private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
private:
    static Singleton instance;
}
//實現文件中
Singleton Singleton::instance;

由於在main函數之前初始化,所以沒有線程安全的問題,但是潛在問題在於no - local static對象(函數外的static對象)在不同編譯單元(可理解爲cpp文件和其包含的頭文件)中的初始化順序是未定義的。如果在初始化完成之前調用 Instance()方法會返回一個未定義的實例。


2.3 Meyers Singleton

Scott Meyers在《Effective C++》(Item 04)中的提出另一種更優雅的單例模式實現,使用local static對象(函數內的static對象)。當第一次訪問Instance()方法時才創建實例。

class Singleton
{
public:
    static Singleton& Instance()
    {
        static Singleton instance;
            return instance;
    }
private:
    Singleton();
        ~Singleton();
        Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
};
C++0x之後是該實現線程安全的,有興趣可以讀相關的標準草案(section 6.7)編譯器支持程度不一定, 但是G++4.0及以上是支持的。


2.4 雙檢測鎖模式(Double - Checked Locking Pattern)

Lazy Singleton的一種線程安全改造是在Instance()中每次判斷是否爲NULL前加鎖,但是加鎖是很慢的。
而實際上只有第一次實例創建的時候才需要加鎖。雙檢測鎖模式被提出來,改造之後大致是這樣

static Singleton& Instance()
{
    if (instance_ == NULL)
    {
        Lock lock; //基於作用域的加鎖,超出作用域,自動調用析構函數解鎖
        if (instance_ == NULL)
        {
            instance_ = new Singleton;
        }
    }
    return *instance_;
}
既然只需要在第一次初始化的時候加鎖,那麼在這之前判斷一下實例有沒有被創建就可以了,所以多在加鎖之前多加一層判斷,需要判斷兩次所有叫Double - Checked。理論上問題解決了,但是在實踐中有很多坑,如指令重排、多核處理器等問題讓DCLP實現起來比較複雜比如需要使用內存屏障,詳細的分析可以閱讀這篇論文。


在C++11中有全新的內存模型和原子庫,可以很方便的用來實現DCLP。這裏不展開。有興趣可以閱讀這篇文章《Double - Checked Locking is Fixed In C++11》。
pthread_once在多線程編程環境下,儘管pthread_once()調用會出現在多個線程中,init_routine()函數僅執行一次,pthread_once是很適合用來實現線程安全單例。


template<typename T>
class Singleton : boost::noncopyable
{
public:
    static T& instance()
    {
        pthread_once(&ponce_, &Singleton::init);
        return *value_;
    }


    static void init()
    {
        value_ = new T();
    }
private:
    static pthread_once_t ponce_;
    static T* value_;
};
template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;


template<typename T>
T* Singleton<T>::value_ = NULL;
這裏的boost::noncopyable的作用是把構造函數, 賦值函數, 析構函數, 複製構造函數聲明爲私有或者保護。


總結
單例模式的實現方法很多,要寫一個完美的實現很難代碼也會很複雜。但是掌握基礎的實現還是很必要的,然後在實際應用中不斷地去優化和探索。除了線程安全,一些場景下還有需要考慮資源釋放,生命週期等相關問題,可以參見《Modern C++ Design》中對Singleton的討論

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章