You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

关于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:58:03