关于C++双重检查锁定模式的线程安全问题求证
关于Double-Checked Locking(DCL)模式的竞态条件求证
先来看你提到的经典DCL实现代码:
Singleton& Singleton::Instance() { if(!pInstance_) { Guard myGuard(lock_); if (!pInstance_) { pInstance_ = new Singleton; } } return *pInstance_; }
你描述的那个执行流程确实是真实存在且完全正确的,这曾是C++11标准发布前DCL模式的致命缺陷,根源在于编译器的指令重排和CPU的乱序执行优化。
为什么会出现这种问题?
我们拆分pInstance_ = new Singleton;的实际执行步骤:
- 步骤1:调用
operator new分配足够容纳Singleton对象的内存 - 步骤2:在这块内存上调用
Singleton的构造函数,完成对象初始化 - 步骤3:将分配好的内存地址赋值给
pInstance_指针
在C11之前,C标准没有对内存操作的执行顺序做严格约束。编译器为了提升性能,完全有权限对步骤2和步骤3进行重排——也就是先把内存地址赋值给pInstance_,再去执行构造函数初始化对象。
这就会触发你描述的场景:线程1已经将非空地址写入pInstance_,但对象还没完成构造;此时线程2进入第一个if(!pInstance_)判断,发现指针非空,直接返回了一个未完全初始化的对象,后续对该对象的操作会引发未定义行为(比如访问未初始化的成员变量、程序崩溃等)。
C++11之后的修复方案
C++11引入了内存模型和std::atomic,专门解决多线程下的内存可见性与指令重排问题。正确的DCL实现需要将pInstance_声明为原子指针,并配合合适的内存序:
#include <atomic> #include <mutex> class Singleton { private: static std::atomic<Singleton*> pInstance_; static std::mutex lock_; Singleton() {} // 私有构造函数,禁止外部实例化 public: static Singleton& Instance() { Singleton* tmp = pInstance_.load(std::memory_order_acquire); if (!tmp) { std::lock_guard<std::mutex> lock(lock_); tmp = pInstance_.load(std::memory_order_relaxed); if (!tmp) { tmp = new Singleton; pInstance_.store(tmp, std::memory_order_release); } } return *tmp; } };
这里的std::memory_order_acquire和std::memory_order_release起到了关键作用:
- 确保线程2通过
load获取到非空指针时,线程1中store之前的所有操作(包括对象构造)都已对线程2可见 - 禁止编译器和CPU对相关内存操作进行重排,彻底避免了"指针先赋值、对象后构造"的问题
总结
你提到的执行流程在C11之前的DCL实现中是完全可能发生的,属于标准未约束内存模型导致的合法优化行为。只有在C11及之后,通过原子操作和内存序的配合,才能真正安全地实现Double-Checked Locking模式。
内容的提问来源于stack exchange,提问作者Eduard Rostomyan
相关产品推荐
相关产品推荐

