保證一個類僅有一個實例,並提供一個該實例的全局訪問點。 ——《設計模式》GoF
在軟件系統中,常常有這樣一些特殊的類,必須保證他們在系統中只存在一個實例,才能確保它們的邏輯正確性、以及良好的效率。html
因此得考慮如何繞過常規的構造器(不容許使用者new出一個對象),提供一種機制來保證一個類只有一個實例。java
應用場景:linux
class Singleton{ public: static Singleton* getInstance(){ // 先檢查對象是否存在 if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance; } private: Singleton(); //私有構造函數,不容許使用者本身生成對象 Singleton(const Singleton& other); static Singleton* m_instance; //靜態成員變量 }; Singleton* Singleton::m_instance=nullptr; //靜態成員須要先初始化
這是單例模式最經典的實現方式,將構造函數和拷貝構造函數都設爲私有的,並且採用了延遲初始化的方式,在第一次調用getInstance()
的時候纔會生成對象,不調用就不會生成對象,不佔據內存。然而,在多線程的狀況下,這種方法是不安全的。c++
分析:正常狀況下,若是線程A
調用getInstance()
時,將m_instance
初始化了,那麼線程B
再調用getInstance()
時,就不會再執行new
了,直接返回以前構造好的對象。然而存在這種狀況,線程A
執行m_instance = new Singleton()
還沒完成,這個時候m_instance
仍然爲nullptr
,線程B
也正在執行m_instance = new Singleton()
,這是就會產生兩個對象,線程A
和B
可能使用的是同一個對象,也多是兩個對象,這樣就可能致使程序錯誤,同時,還會發生內存泄漏。數據庫
//線程安全版本,但鎖的代價太高 Singleton* Singleton::getInstance() { Lock lock; //僞代碼 加鎖 if (m_instance == nullptr) { m_instance = new Singleton(); } return m_instance; }
分析:這種寫法不會出現上面兩個線程都執行new
的狀況,當線程A
在執行m_instance = new Singleton()
的時候,線程B
若是調用了getInstance()
,必定會被阻塞在加鎖處,等待線程A
執行結束後釋放這個鎖。從而是線程安全的。編程
但這種寫法的性能不高,由於每次調用getInstance()
都會加鎖釋放鎖,而這個步驟只有在第一次new Singleton()
纔是有必要的,只要m_instance
被建立出來了,無論多少線程同時訪問,使用if (m_instance == nullptr)
進行判斷都是足夠的(只是讀操做,不須要加鎖),沒有線程安全問題,加了鎖以後反而存在性能問題。c#
上面的作法是無論三七二十一,某個線程要訪問的時候,先鎖上再說,這樣會致使沒必要要的鎖的消耗,那麼,是否能夠先判斷下if (m_instance == nullptr)
呢,若是知足,說明根本不須要鎖啊!這就是所謂的雙檢查鎖(DCL)的思想,DCL即double-checked locking。設計模式
//雙檢查鎖,但因爲內存讀寫reorder不安全 Singleton* Singleton::getInstance() { //先判斷是否是初始化了,若是初始化過,就不再會使用鎖了 if(m_instance==nullptr){ Lock lock; //僞代碼 if (m_instance == nullptr) { m_instance = new Singleton(); } } return m_instance; }
這樣看起來很棒!只有在第一次必要的時候纔會使用鎖,以後就和實現一
中同樣了。安全
在至關長的一段時間,迷惑了不少人,在2000
年的時候才被人發現漏洞,並且在每種語言上都發現了。緣由是內存讀寫的亂序執行(編譯器的問題)。多線程
分析:m_instance = new Singleton()
這句話能夠分紅三個步驟來執行:
Singleton
類型對象所須要的內存。Singleton
類型的對象。m_instance
。可能會認爲這三個步驟是按順序執行的,但實際上只能肯定步驟1
是最早執行的,步驟2
,3
卻不必定。問題就出如今這。假如某個線程A在調用執行m_instance = new Singleton()
的時候是按照1,3,2
的順序的,那麼剛剛執行完步驟3
給Singleton
類型分配了內存(此時m_instance
就不是nullptr
了)就切換到了線程B
,因爲m_instance
已經不是nullptr
了,因此線程B
會直接執行return m_instance
獲得一個對象,而這個對象並無真正的被構造!!嚴重bug就這麼發生了。
java
和c#
發現這個問題後,就加了一個關鍵字volatile
,在聲明m_instance
變量的時候,要加上volatile
修飾,編譯器看到以後,就知道這個地方不可以reorder(必定要先分配內存,在執行構造器,都完成以後再賦值)。
而對於c++
標準卻一直沒有改正,因此VC++
在2005
版本也加入了這個關鍵字,可是這並不可以跨平臺(只支持微軟平臺)。
而到了c++ 11
版本,終於有了這樣的機制幫助咱們實現跨平臺的方案。
//C++ 11版本以後的跨平臺實現 // atomic c++11中提供的原子操做 std::atomic<Singleton*> Singleton::m_instance; std::mutex Singleton::m_mutex; /* * std::atomic_thread_fence(std::memory_order_acquire); * std::atomic_thread_fence(std::memory_order_release); * 這兩句話能夠保證他們之間的語句不會發生亂序執行。 */ Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire);//獲取內存fence if (tmp == nullptr) { std::lock_guard<std::mutex> lock(m_mutex); tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new Singleton; std::atomic_thread_fence(std::memory_order_release);//釋放內存fence m_instance.store(tmp, std::memory_order_relaxed); } } return tmp; }
在linux中,pthread_once()
函數能夠保證某個函數只執行一次。
聲明: int pthread_once(pthread_once_t once_control, void (init_routine) (void)); 功能: 本函數使用初值爲PTHREAD_ONCE_INIT的once_control 變量保證init_routine()函數在本進程執行序列中僅執行一次。
示例以下:
class Singleton{ public: static Singleton* getInstance(){ // init函數只會執行一次 pthread_once(&ponce_, &Singleton::init); return m_instance; } private: Singleton(); //私有構造函數,不容許使用者本身生成對象 Singleton(const Singleton& other); //要寫成靜態方法的緣由:類成員函數隱含傳遞this指針(第一個參數) static void init() { m_instance = new Singleton(); } static pthread_once_t ponce_; static Singleton* m_instance; //靜態成員變量 }; pthread_once_t Singleton::ponce_ = PTHREAD_ONCE_INIT; Singleton* Singleton::m_instance=nullptr; //靜態成員須要先初始化
實現四的方案有點麻煩,實現五的方案不能跨平臺。其實c++ 11
中已經提供了std::call_once
方法來保證函數在多線程環境中只被調用一次,一樣,他也須要一個once_flag
的參數。用法和pthread_once
相似,而且支持跨平臺。
實際上,還有一種最爲簡單的方案!
在C++memory model中對static local variable,說道:The initialization of such a variable is defined to occur the first time control passes through its declaration; for multiple threads calling the function, this means there’s the potential for a race condition to define first.
局部靜態變量不只只會初始化一次,並且仍是線程安全的。
class Singleton{ public: // 注意返回的是引用。 static Singleton& getInstance(){ static Singleton m_instance; //局部靜態變量 return m_instance; } private: Singleton(); //私有構造函數,不容許使用者本身生成對象 Singleton(const Singleton& other); };
這種單例被稱爲Meyers' Singleton
。這種方法很簡潔,也很完美,可是注意:
可是如今都18年了。。新項目通常都支持了c++11
了。
從上面已經知道了單例模式的各類實現方式。可是有沒有感到一點不和諧的地方?若是我class A
須要作成單例,須要這麼改造class A
,若是class B
也須要作成單例,仍是須要這樣改造一番,是否是有點重複勞動的感受?利用c++
的模板語法能夠避免這樣的重複勞動。
template<typename T> class Singleton { public: static T& getInstance() { static T value_; //靜態局部變量 return value_; } private: Singleton(); ~Singleton(); Singleton(const Singleton&); //拷貝構造函數 Singleton& operator=(const Singleton&); // =運算符重載 };
假若有A
,B
兩個類,用Singleton
類能夠很容易的把他們也包裝成單例。
class A{ public: A(){ a = 1; } void func(){ cout << "A.a = " << a << endl; } private: int a; }; class B{ public: B(){ b = 2; } void func(){ cout << "B.b = " << b << endl; } private: int b; }; // 使用demo int main() { Singleton<A>::getInstance().func(); Singleton<B>::getInstance().func(); return 0; }
假如類A
的構造函數具備參數呢?上面的寫法仍是沒有通用性。可使用C++11
的可變參數模板解決這個問題。可是感受實際中這種需求並非不少,由於構造只須要一次,每次getInstance()
傳個參數不是很麻煩嗎。。。
單例模式自己十分簡單,可是實現上卻發現各類麻煩,主要是多線程編程確實是個難點。而對於c++
的對象模型、內存模型,並無什麼深刻的瞭解,還在只知其一;不知其二的階段,仍需努力。
須要注意的一點是,上面討論的線程安全指的是getInstance()
是線程安全的,假如多個線程都獲取類A
的對象,若是隻是隻讀操做,徹底OK,可是若是有線程要修改,有線程要讀取,那麼類A
自身的函數須要本身加鎖防禦,不是說線程安全的單例也能保證修改和讀取該對象自身的資源也是線程安全的。