在看POCO網(wǎng)絡(luò)庫(kù)的時(shí)候,其中實(shí)現(xiàn)了一個(gè)singleton模式,提到了DCLP的不可行性。就去查閱了一下,
找到了這篇文章。原文太長(zhǎng),將其意思大略整理如下。
singleton差不多是所有設(shè)計(jì)模式中最為常見(jiàn)的一個(gè),但卻不是線程安全的。
DCLP就是為了消除此缺點(diǎn)而設(shè)計(jì)出來(lái)的——Double Checked Locking Pattern。但卻仍然是
不可靠的。
一般的singleton實(shí)現(xiàn)為:
// from the header file
class Singleton {
public:
static Singleton* instance();
private:
static Singleton* pInstance;
};
// from the implementation file
Singleton* Singleton::pInstance = 0;
Singleton* Singleton::instance() {
if (pInstance == 0) { //Line 1
pInstance=new Singleton; //Line 2
}
return pInstance;
}
單線程模式下,這種方法工作的很好。但在多線程模式下卻是有問(wèn)題的。
假設(shè)線程A進(jìn)入了instance函數(shù),執(zhí)行到Line 1,然后被掛起。當(dāng)它被掛起的時(shí)候,它
剛測(cè)得pInstance是NULL,所以還沒(méi)有Singleton對(duì)象被創(chuàng)造出來(lái)。
然后線程B進(jìn)入instance并且執(zhí)行到line1,發(fā)現(xiàn)pInstance是NULL,然后執(zhí)行下一句,創(chuàng)建了一個(gè)
Singleton并且使pInstance指向它。然后返回pInstance.
當(dāng)線程A繼續(xù)執(zhí)行的時(shí)候,它會(huì)執(zhí)行Line2,創(chuàng)建一個(gè)Singleton并使pInstance指向它,這就
破壞了singleton的意義,因?yàn)閯?chuàng)建了兩個(gè)singleton.
要想使其是線程安全的,需要在測(cè)試pInstance之間加一個(gè)lock.
Singleton* Singleton::instance() {
Lock lock;
// acquire lock (params omitted for simplicity)
if (pInstance == 0) {
pInstance = new Singleton;
}
return pInstance;
}
// release lock
這樣有一個(gè)和很大的缺點(diǎn)——代價(jià)太高,每次訪問(wèn)都需要一個(gè)lock.但實(shí)際上,我們只需要在
第一次創(chuàng)建的時(shí)候加一個(gè)鎖,而且應(yīng)該是instance第一次被調(diào)用的時(shí)候。如果在運(yùn)行時(shí)
instance被調(diào)用了n次,我們只需要在第一次調(diào)用的時(shí)候加鎖就可以了。DCLP就是為了
解決這個(gè)問(wèn)題而設(shè)計(jì)的--------------去掉那些不必要的LOCK。
Singleton* Singleton::instance() {
if (pInstance == 0) {
// 1st test
Lock lock;
if (pInstance == 0) {
// 2nd test
pInstance = new Singleton;
}
}
return pInstance;
}
DCLP在加鎖之前先測(cè)試pInstance是否為空,盡在其為NULL時(shí)才會(huì)需要一個(gè)LOCK,第二次測(cè)試也是
必要的,因?yàn)橛锌赡芰硪粋€(gè)線程在第一次測(cè)試pInstance和請(qǐng)求LOCK時(shí)執(zhí)行了new.
問(wèn)題之所在:
pInstance=new Singleton;分為以下三步:
1.分配內(nèi)存(sizeof(Singleton).
2.在分配的內(nèi)存上創(chuàng)建一個(gè)Singleton對(duì)象。
3.讓pInstance指向這塊內(nèi)存。問(wèn)題就在于編譯器不一定按照順序執(zhí)行這三步。 有時(shí)候編譯器會(huì)將
第二步和第三步互換。此時(shí)情況可能如以下所示:
Singleton* Singleton::instance() {
if (pInstance == 0) {
Lock lock;
if (pInstance == 0) {
pInstance =
// Step 3
operator new(sizeof(Singleton)); // Step 1
// Step 2
new (pInstance) Singleton;
}
}
return pInstance;
}
在實(shí)際的DCLP代碼中,step2是可能拋出異常的,這時(shí)需要保證pInstance沒(méi)有變化(setp3未
執(zhí)行)。所以一般并不能把step3移到step2之前,但有時(shí)候是可以的,比如說(shuō)step2并不拋出異
常。
現(xiàn)在如果線程A進(jìn)入instance,進(jìn)行第一次測(cè)試,請(qǐng)求了一個(gè)LOCK,然后執(zhí)行了step1和step3.
然后被掛起。此時(shí)pInstance是NO-NULL,但是沒(méi)有singleton對(duì)象被創(chuàng)建出來(lái)。
然后,線程B進(jìn)入instance,發(fā)現(xiàn)pInstance非空,然后將其返回,然后解引用,但是卻沒(méi)有對(duì)象。
所以,DCLP只有在step1和step2在step3之前完成的情況下才能正常工作,但C/C++并不提供這樣的
保證。