前 言
我們在寫程序時,經(jīng)常需要實現(xiàn)某一個類對象能夠全局訪問心赶,但又需要保證其唯一的設(shè)計严沥。這就需要使用常說的單例模式來實現(xiàn)。
實 現(xiàn)
《設(shè)計模式》書中給出了一種實現(xiàn)方式拷沸,即定義一個單例類色查,使用類的私有靜態(tài)指針變量指向類的唯一實例,并用一個公有的靜態(tài)方法來獲取該實例堵漱。
根據(jù)書中的實現(xiàn)方式综慎,示例如下:
class CSingleton
{
private:
CSingleton() //將構(gòu)造函數(shù)設(shè)為私有
static CSingleton *m_pInstance; //私有靜態(tài)指針變量,指向唯一實例
public:
static CSingleton* GetInstance() //公有靜態(tài)方法勤庐,獲取該實例
{
if(m_pInstance == nullptr) //判斷是否第一次調(diào)用
m_pInstance = new CSingleton();
return m_pInstance;
}
};
因為類的構(gòu)造函數(shù)是私有的示惊,所以外部任何創(chuàng)建實例的嘗試都將失敗,訪問實例的唯一方法愉镰,即通過公有靜態(tài)方法GetInstance()
米罚。GetInstance()
返回的實例是當(dāng)這個函數(shù)首次被訪問時創(chuàng)建的。這就是常說的懶漢模式丈探。
以上的實現(xiàn)方式滿足了我們的需求录择,但是存在著諸多問題,比如:該實例如何刪除碗降?
我們可以在程序結(jié)束時調(diào)用GetInstance()并delete掉返回的實例指針隘竭。但是這樣的操作很容易出錯,因為我們很難保證在程序執(zhí)行的最后刪除讼渊;也不能保證刪除掉實例后动看,程序不再調(diào)用創(chuàng)建實例。
我們知道爪幻,系統(tǒng)會在程序結(jié)束后釋放所有全局變量并析構(gòu)所有類的靜態(tài)對象菱皆。利用這一個特性须误,我們可以在類中設(shè)計一個靜態(tài)成員變量,在其析構(gòu)函數(shù)中刪除唯一實例仇轻。在程序結(jié)束時京痢,系統(tǒng)將會調(diào)用這個靜態(tài)成員變量的析構(gòu)函數(shù),從而幫助我們自動的刪除唯一實例篷店,且不會出現(xiàn)人為的意外失誤祭椰。如下面實例中的CGarbo類:
class CSingleton
{
//經(jīng)典單例(Singleton)設(shè)計模式,只創(chuàng)建一個對象疲陕,并且自動釋放
private:
CSingleton(void)
static CSingleton *m_pInstance;
//其唯一作用就是在析構(gòu)函數(shù)中刪除CSingleton實例
class CGarbo
{
public:
~CGarbo()
{
if(CSingleton::m_pInstance)
delete CSingleton::m_pInstance;
}
};
static CGarbo Garbo; //程序結(jié)束時吭产,系統(tǒng)會調(diào)用其析構(gòu)函數(shù)
public:
static CSingleton * GetInstance()
{
if(m_pInstance == nullptr)
m_pInstance = new CSingleton();
return m_pInstance;
}
};
以上的寫法,即滿足了全局訪問唯一實例鸭轮,也保證了在程序結(jié)束時,系統(tǒng)幫助我們選擇正確的釋放時機橄霉,不必我們關(guān)心此實例的釋放窃爷。但是依舊存在缺陷,因為此方式是線程不安全的姓蜂。在多線程中按厘,當(dāng)多個線程同時訪問時,會同時判斷實例未創(chuàng)建钱慢,從而創(chuàng)建出多個實例逮京,很明顯違背了我們實例唯一的需求。
不難想出束莫,此時可以使用線程鎖來保證線程的安全懒棉。如以下示例:
static CSingleton * GetInstance()
{
Lock(); //可以使用臨界區(qū)CRITICAL_SECTION或者互斥量MUTEX來實現(xiàn)線程鎖
if(m_pInstance == nullptr)
m_pInstance = new CSingleton();
UnLock();
return m_pInstance;
}
上面的寫法依舊存在缺陷,因為當(dāng)某個線程要訪問時览绿,就立即上鎖策严,這樣導(dǎo)致了不必要的鎖的消耗。所以我們可以先判斷下實例是否存在饿敲,再進行是否上鎖的操作妻导。這就是所謂的雙檢查鎖(DCL)思想,即Double Checked Locking怀各。優(yōu)化的寫法如下實例:
static CSingleton * GetInstance()
{
if(m_pInstance == nullptr) {
Lock();
if(m_pInstance == nullptr) {
m_pInstance = new CSingleton();
}
UnLock();
}
return m_pInstance ;
}
此時一個完整的單例模式就實現(xiàn)了倔韭,但事實證明,此實現(xiàn)的寫法依舊存在著重大的問題瓢对,而問題就在于m_pInstance = new CSingleton;
這一句寿酌,具體如下:
分析: m_pInstance = new CSingleton()這句話可以分成三個步驟來執(zhí)行:
1.分配了一個CSingleton類型對象所需要的內(nèi)存。
2.在分配的內(nèi)存處構(gòu)造CSingleton類型的對象沥曹。
3.把分配的內(nèi)存的地址賦給指針m_pInstance份名。
可能會認為這三個步驟是按順序執(zhí)行的,但實際上只能確定步驟 1 是最先執(zhí)行的,步驟2,3卻不一定碟联。
問題就出現(xiàn)在這。假如某個線程A在調(diào)用執(zhí)行m_pInstance = new CSingleton()的時候是按照1, 3, 2的順序的,
那么剛剛執(zhí)行完步驟3給singleton類型分配了內(nèi)存(此時m_ instance就不是nullptr了 )就切換到了線程B,
由于m_pInstance已經(jīng)不是nullptr了,所以線程B會直接執(zhí)行return m_ instance得到一個對象,而這個對象并沒有真正的被構(gòu)造! !
嚴重bug就這么發(fā)生了僵腺。
參考:https://segmentfault.com/a/1190000015950693
進一步探討
著名的《Effective C++》系列書籍的作者 Meyers 提出了C++ 11版本最簡潔的跨平臺方案鲤孵,即Meyers' Singleton
實現(xiàn)如下:
class CSingleton
{
private:
CSingleton(void)
public:
static CSingleton & getInstance()
{
static CSingleton m_pInstance; //局部靜態(tài)變量
return m_pInstance;
}
};
這樣的寫法即簡潔又完美!需要注意的是此寫法需要支持C++11以上辰如、GCC4.0編譯器以上普监。
以上的所有內(nèi)容是本人的一些思考和領(lǐng)悟,如有不正確的地方琉兜,歡迎大佬指正凯正。