DCLP在C++11+可安全实现,但必须用std::atomic<T*>配对memory_order_acquire/release;volatile或relaxed仍导致未定义行为。

直接说结论:DCLP(双重检查锁定)在 C++11 及以后可以安全实现,但必须用 std::atomic<T*> + memory_order_acquire/memory_order_release 配对;仅用 volatile 或 memory_order_relaxed 仍会触发未定义行为。
为什么经典 DCLP 在 C++11 前不安全
问题不在“加锁”本身,而在构造与指针发布的重排序。比如线程 A 执行 instance = new Singleton() 时,编译器或 CPU 可能将“分配内存 → 写入 instance 指针 → 调用构造函数”重排为“分配内存 → 写入 instance → 构造函数”,导致线程 B 看到非空 instance,却访问到未初始化的成员——段错误或随机值是常见表现。
这和 Java 的 volatile 语义不同:C++ 的 volatile 不提供任何线程同步保证,不能替代原子操作。
std::atomic<T*> 的正确用法
关键不是“让指针读写原子”,而是通过内存序约束整个构造过程的可见性顺序:
立即学习“C++免费学习笔记(深入)”;
-
instance_.load(std::memory_order_acquire)用于首次无锁检查:它确保后续对instance_所指对象的读取,不会被重排到该load之前 -
instance_.store(ptr, std::memory_order_release)用于发布指针:它确保此前所有对ptr对象成员的写入(即构造),对其他线程可见 - 禁止混用
memory_order_relaxed:例如if (instance_.load(std::memory_order_relaxed)) return instance_.load(std::memory_order_relaxed)—— 两次load可能返回不同值,且不保证构造完成 -
instance_必须是静态存储期变量(如static std::atomic<Singleton*> instance_{nullptr}),不能是局部静态变量(否则退化为 Meyers 单例)
比 DCLP 更推荐的替代方案
DCLP 的目标是“首次创建同步、后续读取无锁”,但实际中维护成本高、易出错。更务实的选择:
-
std::call_once+std::once_flag:异常安全、语义清晰、标准库保障,性能损耗可忽略。它底层仍是锁+原子状态机,但你不用操心内存序 - Meyers 单例(静态局部变量):C++11 起,
static Singleton& instance() { static Singleton inst; return inst; }是线程安全且延迟加载的,由编译器生成栅栏指令,无需手写原子逻辑 - 饿汉式(静态成员):若初始化开销小、无依赖循环,直接在定义处构造,最简单可靠
真正需要 DCLP 的场景极少——比如单例构造极重、且读取频次极高(百万/秒以上)、对分支预测失败极其敏感。这种情况下,连 std::call_once 的一次原子读+条件跳转都可能成为瓶颈,才值得投入精力手写 std::atomic + 内存屏障。
容易被忽略的陷阱
最常踩的坑不是“没加锁”,而是“以为加了原子就万事大吉”:
- 误把
new Singleton()当作原子操作:new表达式本身有memory_order_seq_cst语义,但仅限于内存分配;若构造函数内含非原子写(如初始化std::vector、调用虚函数等),仍需memory_order_release保证整体完成 - 忘记
std::atomic_thread_fence(std::memory_order_release):当构造体含非平凡初始化(如 placement new 后手动调用构造)时,仅靠store不足以同步所有写入 - 用
std::atomic_flag替代时,未用ATOMIC_FLAG_INIT初始化,导致未定义行为
说到底,DCLP 的复杂性不在“双重检查”,而在“如何让其他线程确信:我不仅写了指针,而且对象真 ready 了”。这个“确信”,必须由内存序显式建立,而不是靠直觉或注释。


















