一 、定義
衆多設計模式中,單例模式是一種比較常見模式。本文以一個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的討論