【C++】C/C++ 中的单例模式
目錄
?
part 0:單例模式3種經典的實現方式
Meyer's Singleton
Meyers Singleton版本二
Lazy Singleton
Eager Singleton
Testing
part 1:C++之單例模式
動機
實現一[線程不安全版本]
實現二[線程安全,鎖的代價過高]
鎖機制
實現三[雙檢查鎖,由于內存讀寫reoder導致不安全]
實現四[C++ 11版本的跨平臺實現]
實現五[pthread_once函數]
另外一個版本實現std::call_once & std::once_flag
實現六[c++ 11版本最簡潔的跨平臺方案]
用模板包裝單例
總結
part 2:單例模式
1.傳統的單例模式實現
part 3:java之單例模式
介紹
實現
步驟 1
SingleObject.java
步驟 2
SingletonPatternDemo.java
步驟 3
單例模式的幾種實現方式
1、懶漢式,線程不安全
實例
2、懶漢式,線程安全
實例
3、餓漢式
實例
4、雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking)
實例
5、登記式/靜態內部類
實例
6、枚舉
實例
part 0:單例模式3種經典的實現方式
單例模式是一種創建型的設計模式(creational design patterns),使用單例模式進行設計的類在程序中只擁有一個實例(single instance),這個類稱為單例類,它會提供一個全局的訪問入口(global access point),關于單例模式的討論可以參考Singleton revisited;基于這兩個特點,單例模式可以有以下幾種實現:
Meyer's Singleton
Scott Meyers 在 Effective C++ 的 Item 4: Make sure that objects are initialized before they're used 里面提出了一種利用 C++ 的 static 關鍵字來實現的單例模式,這種實現非常簡潔高效,它的特點是:
-
僅當程序第一次執行到
GetInstance函數時,執行instance對象的初始化; -
在 C++ 11 之后,被
static修飾的變量可以保證是線程安全的;template<typename T> class Singleton { public:static T& GetInstance(){static T instance;return instance;} ?Singleton(T&&) = delete;Singleton(const T&) = delete;void operator= (const T&) = delete; ? protected:Singleton() = default;virtual ~Singleton() = default; };通過禁用單例類的 copy constructor,move constructor 和 operator= 可以防止類的唯一實例被拷貝或移動;不暴露單例類的 constructor 和 destructor 可以保證單例類不會通過其他途徑被實例化,同時將兩者定義為 protected 可以讓其被子類繼承并使用。
Meyers Singleton版本二
Meyers Singleton的實現方式基于"static variables with block scope"的自動線程安全特性,非常簡單易懂。
class MeyersSingleton{ public:static MySingleton& getInstance(){static MySingleton instance;// volatile int dummy{};return instance;} private:MySingleton()= default;~MySingleton()= default;MySingleton(const MySingleton&)= delete;MySingleton& operator=(const MySingleton&)= delete; };Lazy Singleton
Lazy Singleton 是一種比較傳統的實現方法,通過其名字可以看出來它也具有 lazy-evaluation 的特點,但在實現的時候需要考慮線程安全的問題:
template<typename T, bool is_thread_safe = true> class LazySingleton { private:static unique_ptr<T> t_;static mutex mtx_; ? public:static T& GetInstance(){if (is_thread_safe == false){if (t_ == nullptr)t_ = unique_ptr<T>(new T);return *t_;} ?if (t_ == nullptr){unique_lock<mutex> unique_locker(mtx_);if (t_ == nullptr)t_ = unique_ptr<T>(new T);return *t_;} ?} ?LazySingleton(T&&) = delete;LazySingleton(const T&) = delete;void operator= (const T&) = delete; ? protected:LazySingleton() = default;virtual ~LazySingleton() = default; }; ? template<typename T, bool is_thread_safe> unique_ptr<T> LazySingleton<T, is_thread_safe>::t_; ? template<typename T, bool is_thread_safe> mutex LazySingleton<T, is_thread_safe>::mtx_;我們通過模板參數
is_thread_safe來控制這個類是否是線程安全的,因為在某些場景下我們會希望每個線程擁有一個實例:-
當
is_thread_safe == false,即非線程安全時,我們在GetInstance函數中直接判斷,初始化并返回單例對象;這里使用了unique_ptr防止線程銷毀時發生內存泄漏,也可以在析構函數中銷毀指針; -
當
is_thread_safe == true時,我們通過 double-checked locking 來進行檢查并加鎖,防止單例類在每個線程上都被實例化。
Eager Singleton
和 Lazy Singleton 相反,Eager Singleton 利用 static member variable 的特性,在程序進入 main 函數之前進行初始化,這樣就繞開了線程安全的問題:
template<typename T> class EagerSingleton { private:static T* t_; ? public:static T& GetInstance(){return *t_;} ?EagerSingleton(T&&) = delete;EagerSingleton(const T&) = delete;void operator= (const T&) = delete; ? protected:EagerSingleton() = default;virtual ~EagerSingleton() = default; }; ? template<typename T> T* EagerSingleton<T>::t_ = new (std::nothrow) T;但是它也有兩個問題:
-
即使單例對象不被使用,單例類對象也會進行初始化;
-
static initialization order fiasco,即 t_ 對象和
GetInstance函數的初始化先后順序是不固定的;Testing
將上面實現的四種 Singleton 分別繼承下來作為 functor 傳入線程對象進行測試:
class Foo : public Singleton<Foo> { public:void operator() (){cout << &GetInstance() << endl;} }; ? class LazyFoo : public LazySingleton<LazyFoo, false> { public:void operator() (){cout << &GetInstance() << endl;} }; ? class ThreadSafeLazyFoo : public LazySingleton<ThreadSafeLazyFoo> { public:void operator() (){cout << &GetInstance() << endl;} }; ? class EagerFoo : public EagerSingleton<EagerFoo> { public:void operator() (){cout << &GetInstance() << endl;} }; ? void SingletonTest() {thread t1((Foo()));thread t2((Foo()));t1.join();t2.join();this_thread::sleep_for(chrono::milliseconds(100)); ?t1 = thread((LazyFoo()));t2 = thread((LazyFoo()));t1.join();t2.join();this_thread::sleep_for(chrono::milliseconds(100)); ?t1 = thread((ThreadSafeLazyFoo()));t2 = thread((ThreadSafeLazyFoo()));t1.join();t2.join();this_thread::sleep_for(chrono::milliseconds(100)); ?t1 = thread((EagerFoo()));t2 = thread((EagerFoo()));t1.join();t2.join(); }輸出結果為:
0x60d110 0x60d110 0x7f92380008c0 0x7f92300008c0 0x7f92300008e0 0x7f92300008e0 0x1132010 0x1132010可以看到只有第二組非線程安全的
LazySingleton在兩個線程中輸出的實例地址是不同的,其它的 Singleton 均是線程安全的。
-
part 1:C++之單例模式
動機
保證一個類僅有一個實例,并提供一個該實例的全局訪問點。 ——《設計模式》GoF
在軟件系統中,經常有這樣一些特殊的類,必須保證他們在系統中只存在一個實例,才能確保它們的邏輯正確性、以及良好的效率。
所以得考慮如何繞過常規的構造器(不允許使用者new出一個對象),提供一種機制來保證一個類只有一個實例。
應用場景:
-
Windows的Task Manager(任務管理器)就是很典型的單例模式,你不能同時打開兩個任務管理器。Windows的回收站也是同理。
-
如連接池、類工廠、文件系統等。這就是設計模式中的單例模式(Singleton Pattern)。
-
應用程序的日志應用,一般都可以用單例模式實現,只能有一個實例去操作文件。
-
讀取配置文件,讀取的配置項是公有的,一個地方讀取了所有地方都能用,沒有必要所有的地方都能讀取一遍配置。
-
數據庫連接池,多線程的線程池。
實現一[線程不安全版本]
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()的時候才會生成對象,不調用就不會生成對象,不占據內存。然而,在多線程的情況下,這種方法是不安全的。分析:正常情況下,如果線程
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)進行判斷都是足夠的(只是讀操作,不需要加鎖),沒有線程安全問題,加了鎖之后反而存在性能問題。鎖機制
std::mutex myMutex; ? class MySingleton{ public:static MySingleton& getInstance(){std::lock_guard<std::mutex> myLock(myMutex);if ( !instance ){instance= new MySingleton();}// volatile int dummy{};return *instance;} private:MySingleton()= default;~MySingleton()= default;MySingleton(const MySingleton&)= delete;MySingleton& operator=(const MySingleton&)= delete; ?static MySingleton* instance; }; ? ? MySingleton* MySingleton::instance= nullptr;每次getInstance方法調用,都需要申請和釋放鎖,開銷非常大。
實現三[雙檢查鎖,由于內存讀寫reoder導致不安全]
上面的做法是不管三七二十一,某個線程要訪問的時候,先鎖上再說,這樣會導致不必要的鎖的消耗,那么,是否可以先判斷下
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就這么發生了。實現四[C++ 11版本的跨平臺實現]
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);//獲取內存fenceif (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);//釋放內存fencem_instance.store(tmp, std::memory_order_relaxed);}}return tmp; }實現五[pthread_once函數]
在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; //靜態成員需要先初始化另外一個版本實現std::call_once & std::once_flag
這種方式基于C++新特性,保證多線程下實例化方法只被調用一次。
class CallOnceSingleton{ public:static MySingleton& getInstance(){std::call_once(initInstanceFlag, &MySingleton::initSingleton);// volatile int dummy{};return *instance;} private:MySingleton()= default;~MySingleton()= default;MySingleton(const MySingleton&)= delete;MySingleton& operator=(const MySingleton&)= delete;static MySingleton* instance;static std::once_flag initInstanceFlag;static void initSingleton(){instance= new MySingleton;} }; ? MySingleton* MySingleton::instance= nullptr; std::once_flag MySingleton::initInstanceFlag;實現六[c++ 11版本最簡潔的跨平臺方案]
實現四的方案有點麻煩,實現五的方案不能跨平臺。其實
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。這種方法很簡潔,也很完美,但是注意:-
gcc 4.0之后的編譯器支持這種寫法。
-
C++11及以后的版本(如C++14)的多線程下,正確。
-
C++11之前不能這么寫。
但是現在都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自身的函數需要自己加鎖防護,不是說線程安全的單例也能保證修改和讀取該對象自身的資源也是線程安全的。?
-
part 2:單例模式
?
這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
注意:
-
1、單例類只能有一個實例。
-
2、單例類必須自己創建自己的唯一實例。
-
3、單例類必須給所有其他對象提供這一實例。
1.傳統的單例模式實現
class Singleton
{
private:Singleton(){}
public:static Singleton* instance(){if(_instance == 0){_instance = new Singleton();}return _instance;}
private:static Singleton* _instance;public:int atestvalue;
};Singleton* Singleton::_instance = 0;
上面這種實現在單線程環境下是沒有問題的,可是多線程下就有問題了。
當:
-
例如線程A進入函數instance執行判斷語句,這句執行后就掛起了,這時線程A已經認為_instance為NULL,但是線程A還沒有創建singleton對象。
-
又有一個線程B進入函數instance執行判斷語句,此時同樣認為_instance變量為null,因為A沒有創建singleton對象。線程B繼續執行,創建了一個singleton對象。
-
稍后,線程A接著執行,也創建了一個新的singleton對象。
-
這時,單例就會同時創建2個對象。
?
針對上面的分析可以看出,需要對_instance變量加上互斥鎖:
Singleton* Singleton::instance() {Lock lock; // acquire lock (params omitted for simplicity)if (_instance == 0) {_instance = new Singleton;}return _instance;
} // release lock (via Lock destructor)
上鎖后是解決了線程安全問題,但是有些資源浪費。稍微分析一下:每次instance函數調用時候都需要請求加鎖,其實并不需要,instance函數只需第一次調用的時候上鎖就行了。這時可以用DCLP解決。
Singleton* Singleton::instance() {if (_instance == 0) { // 1st testLock lock;if (_instance == 0) { // 2nd test_instance = new Singleton;}}return _instance;
}
站在編譯器的角度關注下這句代碼的執行順序:
_instance = new singleton()
為了執行這句代碼,機器需要做三樣事兒:
1.singleton對象分配空間。
2.在分配的空間中構造對象
3.使_instance指向分配的空間
遺憾的是編譯器并不是嚴格按照上面的順序來執行的。可以交換2和3.
將上面三個步驟標記到代碼中就是這樣:
Singleton* Singleton::instance() {if (_instance == 0) {Lock lock;if (_instance == 0) {_instance = // Step 3operator new(sizeof(Singleton)); // Step 1new (_instance) Singleton; // Step 2}}return _instance;
}
好了,緊張的時刻到了,如果發生下面兩件事:
線程A進入了instance函數,并且執行了step1和step3,然后掛起。這時的狀態是:instance不NULL,而instance指向的內存去沒有對象! 線程B進入了instance函數,發現_instance不為null,就直接return _instance了。 貌似這時無法解決的問題了,咋辦呢。搞嵌入式的程序員可能想到用c++中的volatile關鍵字。對,就是用volatile,但是用volatile就要一用到底,用了之后就是下面這種丑陋的代碼了。
class Singleton {
public:static volatile Singleton* volatile instance();
...
private:
// one more volatile addedstatic Singleton* volatile _instance;
};// from the implementation file
volatile Singleton* volatile Singleton::_instance = 0;
volatile Singleton* volatile Singleton::instance() {if (_instance == 0) {Lock lock;if (_instance == 0) {// one more volatile addedSingleton* volatile temp = new Singleton;_instance = temp;}}return _instance;
}
其實上面完全使用volatile關鍵字的代碼也不能保證正常工作在多線程環境中。具體原因分析請參考C++ and the Perils of Double-Checked Locking這篇論文,文章也給出了終極解決方法。
part 3:java之單例模式
介紹
意圖:保證一個類僅有一個實例,并提供一個訪問它的全局訪問點。
主要解決:一個全局使用的類頻繁地創建與銷毀。
何時使用:當您想控制實例數目,節省系統資源的時候。
如何解決:判斷系統是否已經有這個單例,如果有則返回,如果沒有則創建。
關鍵代碼:構造函數是私有的。
應用實例:
-
1、一個班級只有一個班主任。
-
2、Windows 是多進程多線程的,在操作一個文件的時候,就不可避免地出現多個進程或線程同時操作一個文件的現象,所以所有文件的處理必須通過唯一的實例來進行。
-
3、一些設備管理器常常設計為單例模式,比如一個電腦有兩臺打印機,在輸出的時候就要處理不能兩臺打印機打印同一個文件。
優點:
-
1、在內存里只有一個實例,減少了內存的開銷,尤其是頻繁的創建和銷毀實例(比如管理學院首頁頁面緩存)。
-
2、避免對資源的多重占用(比如寫文件操作)。
缺點:沒有接口,不能繼承,與單一職責原則沖突,一個類應該只關心內部邏輯,而不關心外面怎么樣來實例化。
使用場景:
-
1、要求生產唯一序列號。
-
2、WEB 中的計數器,不用每次刷新都在數據庫里加一次,用單例先緩存起來。
-
3、創建的一個對象需要消耗的資源過多,比如 I/O 與數據庫的連接等。
注意事項:getInstance() 方法中需要使用同步鎖 synchronized (Singleton.class) 防止多線程同時進入造成 instance 被多次實例化。
實現
我們將創建一個 SingleObject 類。SingleObject 類有它的私有構造函數和本身的一個靜態實例。
SingleObject 類提供了一個靜態方法,供外界獲取它的靜態實例。SingletonPatternDemo 類使用 SingleObject 類來獲取 SingleObject 對象。
步驟 1
創建一個 Singleton 類。
SingleObject.java
public class SingleObject { //創建 SingleObject 的一個對象 private static SingleObject instance = new SingleObject(); //讓構造函數為 private,這樣該類就不會被實例化 private SingleObject(){} //獲取唯一可用的對象 public static SingleObject getInstance(){ return instance; } public void showMessage(){ System.out.println("Hello World!"); } }
步驟 2
從 singleton 類獲取唯一的對象。
SingletonPatternDemo.java
public class SingletonPatternDemo { public static void main(String[] args) { //不合法的構造函數 //編譯時錯誤:構造函數 SingleObject() 是不可見的 //SingleObject object = new SingleObject(); //獲取唯一可用的對象 SingleObject object = SingleObject.getInstance(); //顯示消息 object.showMessage(); } }
步驟 3
執行程序,輸出結果:
Hello World!
單例模式的幾種實現方式
單例模式的實現有多種方式,如下所示:
1、懶漢式,線程不安全
是否 Lazy 初始化:是
是否多線程安全:否
實現難度:易
描述:這種方式是最基本的實現方式,這種實現最大的問題就是不支持多線程。因為沒有加鎖 synchronized,所以嚴格意義上它并不算單例模式。 這種方式 lazy loading 很明顯,不要求線程安全,在多線程不能正常工作。
實例
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
接下來介紹的幾種實現方式都支持多線程,但是在性能上有所差異。
2、懶漢式,線程安全
是否 Lazy 初始化:是
是否多線程安全:是
實現難度:易
描述:這種方式具備很好的 lazy loading,能夠在多線程中很好的工作,但是,效率很低,99% 情況下不需要同步。 優點:第一次調用才初始化,避免內存浪費。 缺點:必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。 getInstance() 的性能對應用程序不是很關鍵(該方法使用不太頻繁)。
實例
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
3、餓漢式
是否 Lazy 初始化:否
是否多線程安全:是
實現難度:易
描述:這種方式比較常用,但容易產生垃圾對象。 優點:沒有加鎖,執行效率會提高。 缺點:類加載時就初始化,浪費內存。 它基于 classloader 機制避免了多線程的同步問題,不過,instance 在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是調用 getInstance 方法, 但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化 instance 顯然沒有達到 lazy loading 的效果。
實例
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }
4、雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking)
JDK 版本:JDK1.5 起
是否 Lazy 初始化:是
是否多線程安全:是
實現難度:較復雜
描述:這種方式采用雙鎖機制,安全且在多線程情況下能保持高性能。 getInstance() 的性能對應用程序很關鍵。
實例
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
5、登記式/靜態內部類
是否 Lazy 初始化:是
是否多線程安全:是
實現難度:一般
描述:這種方式能達到雙檢鎖方式一樣的功效,但實現更簡單。對靜態域使用延遲初始化,應使用這種方式而不是雙檢鎖方式。這種方式只適用于靜態域的情況,雙檢鎖方式可在實例域需要延遲初始化時使用。 這種方式同樣利用了 classloader 機制來保證初始化 instance 時只有一個線程,它跟第 3 種方式不同的是:第 3 種方式只要 Singleton 類被裝載了,那么 instance 就會被實例化(沒有達到 lazy loading 效果),而這種方式是 Singleton 類被裝載了,instance 不一定被初始化。因為 SingletonHolder 類沒有被主動使用,只有通過顯式調用 getInstance 方法時,才會顯式裝載 SingletonHolder 類,從而實例化 instance。想象一下,如果實例化 instance 很消耗資源,所以想讓它延遲加載,另外一方面,又不希望在 Singleton 類加載時就實例化,因為不能確保 Singleton 類還可能在其他的地方被主動使用從而被加載,那么這個時候實例化 instance 顯然是不合適的。這個時候,這種方式相比第 3 種方式就顯得很合理。
實例
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
6、枚舉
JDK 版本:JDK1.5 起
是否 Lazy 初始化:否
是否多線程安全:是
實現難度:易
描述:這種實現方式還沒有被廣泛采用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止多次實例化。 這種方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機制,防止反序列化重新創建新的對象,絕對防止多次實例化。不過,由于 JDK1.5 之后才加入 enum 特性,用這種方式寫不免讓人感覺生疏,在實際工作中,也很少用。 不能通過 reflection attack 來調用私有構造方法。
實例
public enum Singleton { INSTANCE; public void whateverMethod() { } }
經驗之談:一般情況下,不建議使用第 1 種和第 2 種懶漢方式,建議使用第 3 種餓漢方式。只有在要明確實現 lazy loading 效果時,才會使用第 5 種登記方式。如果涉及到反序列化創建對象時,可以嘗試使用第 6 種枚舉方式。如果有其他特殊的需求,可以考慮使用第 4 種雙檢鎖方式。
參考:
-
Scott Meyers. Effective C++:55 Specific Ways to Improve Your Programs and Designs,3rd Edition. 電子工業出版社, 2011
-
Stanley B. Lippman. 深度探索C++對象模型. 電子工業出版社, 2012
-
Scott Meyers. C++ and the Perils of Double-Checked Locking. 2004
-
陳良喬(譯). C++11 FAQ中文版
-
Bjarne Stroustrup. C++11 FAQ
-
Paul E. McKenney, Hans-J. Boehm, Lawrence Crowl. C++ Data-Dependency Ordering: Atomics and Memory Model. 2008
-
Wikipedia. Out-of-order execution
-
Lo?c. Mutex And Memory Visibility, 2009
-
Randal E.Bryant, David O'Hallaron. 深入理解計算機系統(第2版). 機械工業出版社, 2010
-
Martin Thompson. Memory Barriers/Fences, 2011
-
Working Draft, Standard For Programing Language C++. 2012
-
W.Richard Stevens. UNIX環境高級編程(第3版), 人民郵電出版社, 2014
-
stackoverflow. Is Meyers implementation of Singleton pattern thread safe
-
stackoverflow. When are static C++ class members initialized
-
https://segmentfault.com/a/1190000015950693
-
https://www.cnblogs.com/liyuan989/p/4264889.html
總結
以上是生活随笔為你收集整理的【C++】C/C++ 中的单例模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 隆江卤猪脚可以盖盖子吗
- 下一篇: 【leetcode】二叉树与经典问题