c++实现基于双重检查锁的懒汉单例 _ std::atomic内存顺序实战【源码】

双重检查锁里为什么必须用 std::atomic 而不能用普通指针

因为普通指针的读写不保证原子性,更无法阻止编译器或 CPU 的重排序。在多线程首次调用 getInstance() 时,可能看到未构造完成的对象(例如:指针已非空,但构造函数还没执行完),导致崩溃或未定义行为。

典型现象是程序偶发 crash 在单例对象的某个成员函数调用上,堆栈显示对象地址非空但内部字段为垃圾值——这就是“半构造对象”被其他线程访问了。

std::atomic<t></t> 提供原子读写,且能通过内存序控制指令重排边界必须搭配 memory_order_acquire(读)和 memory_order_release(写),否则仍可能出问题不能只靠 volatile 或 std::mutex 外层保护来“掩盖”这个问题:外层锁解决不了初始化过程中的内存可见性

std::call_once 和双重检查锁,该选哪个

std::call_once 更安全、更简洁,是 C++11 推荐的标准方案;双重检查锁是手动实现,容易写错内存序或漏掉 atomic_thread_fence。

但如果你需要延迟初始化 + 高频读取(比如每微秒调用一次 getInstance()),双重检查锁在无竞争时是纯原子读(load(memory_order_acquire)),比 std::call_once 的内部互斥开销更低。

立即学习“C++免费学习笔记(深入)”;

用 std::call_once:代码少、不易错、适合绝大多数场景用双重检查锁:仅当性能压测确认 std::call_once 成为瓶颈,且你清楚 memory_order_relaxed / acquire / release 的边界语义别混用:不要在双重检查锁里再套一层 std::call_once,那既没收益又增复杂度

双重检查锁的完整可运行写法(C++11 及以上)

关键点不在“加锁”,而在两次检查之间对指针的原子操作与内存序配合。下面是最小可靠实现:

class Singleton {public: static Singleton* getInstance() { Singleton* instance = instance_.load(std::memory_order_acquire); if (instance == nullptr) { std::lock_guard<std::mutex> lock(mutex_); instance = instance_.load(std::memory_order_relaxed); if (instance == nullptr) { instance = new Singleton(); instance_.store(instance, std::memory_order_release); } } return instance; }<p>private:Singleton() = default;~Singleton() = default;Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;</p><pre class="brush:php;toolbar:false;">static std::atomic<Singleton*> instance_;static std::mutex mutex_;

};

std::atomic Singleton::instance{nullptr};std::mutex Singleton::mutex;

注意:load(std::memory_order_relaxed) 在加锁后第二次读是安全的,因为锁本身提供了同步;而首次读必须用 acquire,确保后续读能看到 release 写入的完整对象状态。

最容易被忽略的坑:静态局部变量不是万能解

C++11 规定静态局部变量的初始化是线程安全的(即所谓“Meyers 单例”),但它不等于懒汉+双重检查锁——它本质是编译器插入了类似 std::call_once 的逻辑,且无法控制构造时机以外的行为(比如无法在构造前做资源预检、无法定制错误处理路径)。

更隐蔽的问题是:如果单例构造函数抛异常,静态局部变量版本会每次调用都重试构造,而双重检查锁版本一旦失败,instance_ 仍为 nullptr,下次调用可重新尝试(需你自己加异常捕获逻辑)。

静态局部变量够用?优先用它。它简洁、标准、无内存序风险需要构造失败后重试、或构造前 hook、或严格控制对象生命周期(比如配合 atexit 销毁)?才值得上双重检查锁别以为把 static Singleton s; 写在函数里就自动获得高性能——它的首次调用开销和 std::call_once 基本一致

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。