單例模式是設計模式中最常用也最容易出錯的一種。為了在多線程環境下實現線程安全且高效的單例,雙重檢查鎖定模式(Double-Checked Locking Pattern,DCLP)曾被廣泛推崇。然而,在C/C++中,DCLP存在嚴重的根本性缺陷。本文深入剖析其失效原因。
最基本的單例實現:
Singleton* Singleton::instance() {
if (pInstance == 0) { // Line 1
pInstance = new Singleton; // Line 2
}
return pInstance;
}
單線程下完美工作,但多線程下會出現競態條件:線程A檢查到pInstance為空,被掛起;線程B創建實例並返回;線程A恢復執行,再次創建實例,導致兩個實例並發存在,違背單例初衷。
在檢查前加全局鎖雖能保證安全,但每次調用都需加鎖解鎖,性能開銷巨大。實際上只有首次創建需要同步,後續調用直接返回即可。
Singleton* Singleton::instance() {
if (pInstance == 0) { // 第一次檢查
Lock lock;
if (pInstance == 0) { // 第二次檢查
pInstance = new Singleton;
}
}
return pInstance;
}
意圖:第一次檢查無鎖快速通過,僅在實例未創建時獲取鎖,第二次檢查確保無其他線程搶先創建。理論完美,實際卻暗藏殺機。
pInstance = new Singleton 並非原子操作,編譯器通常分三步執行:
operator new(sizeof(Singleton))關鍵問題:編譯器為優化性能,可能將步驟2和3重排序(先賦值指針,再構造對象)。這在單線程無影響,但在多線程下災難性後果:
C++98/03標準未定義線程模型,編譯器優化器在單線程語義下自由重排指令。即使C++11引入內存模型,new表達式的三步操作仍無順序保證,除非顯式使用std::atomic配合內存屏障。
Singleton& Singleton::instance() {
static Singleton instance; // C++11保證線程安全初始化
return instance;
}
編譯器自動生成線程安全的初始化代碼,無鎖、高效、正確。
std::once_flag flag;
Singleton* Singleton::instance() {
std::call_once(flag, []{ pInstance = new Singleton; });
return pInstance;
}
std::atomic pInstance{nullptr};
std::mutex mtx;
Singleton* Singleton::instance() {
Singleton* tmp = pInstance.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard lock(mtx);
tmp = pInstance.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton;
pInstance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
關鍵點:std::atomic配合memory_order_acquire/release建立happens-before關係,禁止關鍵指令重排序。
DCLP在C/C++中失效的根本原因是編譯器優化導致的指令重排序,在缺乏內存模型保證的舊標準下無解。現代C++提供了正確、高效的替代方案,應優先使用語言級別的線程安全初始化機制,避免手寫易錯的同步邏輯。