double-checked locking 在 c++ 中易出错,因编译器重排和 cpu 乱序执行可能导致未初始化对象被访问;正确做法是用 std::atomic 搭配 memory_order 或更推荐的 std::call_once / 静态局部变量。

double-checked locking 在 C++ 中为什么容易出错
因为编译器重排和 CPU 乱序执行,new Singleton() 可能被拆成“分配内存 → 构造对象 → 赋值给指针”三步,而前两步顺序不保证。线程 A 执行到“分配内存 + 赋值指针”,但构造还没完成,线程 B 就看到 m_instance 非空,直接返回未初始化的对象——触发未定义行为。
正确实现必须用 std::atomic 和 memory_order
不能用原始指针或 volatile(它在 C++ 中对多线程同步无效)。核心是:指针读写必须原子,且构造完成前禁止指令越过屏障。
-
std::atomic<singleton> m_instance{nullptr}</singleton>替代裸指针 - 第一次检查用
load(std::memory_order_acquire)(防止后续读被提前) - 赋值时用
store(ptr, std::memory_order_release)(确保构造已结束) - 中间 new 操作需配合
std::atomic_thread_fence(std::memory_order_acquire)或更稳妥地——用std::call_once配合std::once_flag(推荐)
示例关键片段:
class Singleton {
static std::atomic<singleton> m_instance;
static std::once_flag m_once;
Singleton() = default;
public:
static Singleton* getInstance() {
Singleton* ptr = m_instance.load(std::memory_order_acquire);
if (ptr == nullptr) {
std::call_once(m_once, []() {
ptr = new Singleton();
m_instance.store(ptr, std::memory_order_release);
});
}
return ptr;
}
};</singleton>
为什么现在更推荐 std::call_once 而不是手写 double-checked
手写 DCL 容易漏掉 fence、用错 memory_order、或忘记静态局部变量的线程安全特性。C++11 起,函数内静态变量初始化本身就是线程安全的,且由标准保证——比自己拼原子操作更简洁可靠。
- 静态局部变量初始化自动带“首次调用时原子检查 + 构造保护”,无需额外同步原语
- 代码量少、无 memory_order 认知负担、无平台差异风险
- 性能几乎无差别:主流编译器(GCC/Clang/MSVC)对静态局部变量都做了 fast-path 优化
- 唯一限制:构造函数不能抛异常(否则可能重复调用),若需异常安全,仍得兜底用
std::call_once
最简安全写法:
Singleton& getInstance() {
static Singleton instance; // C++11 guaranteed thread-safe
return instance;
}
Windows 上用 InterlockedCompareExchangePointer 的坑
这是 Win32 API 提供的原子指针操作,但它的内存序语义弱于 C++ 标准要求:默认只提供 acquire/release 效果,不隐含 compiler barrier。如果不用 #pragma intrinsic(_ReadWriteBarrier) 或 std::atomic_thread_fence 补齐,编译器仍可能把构造体内的赋值重排到 store 之后。
- 不要单独依赖 Win32 API 实现跨平台 DCL
- 即使只跑 Windows,也优先用
std::atomic——它在 MSVC 下会自动映射为最优指令(如lock xchg),且语义明确 - 若必须用 Win32 原语,务必在 new 后、store 前加
std::atomic_thread_fence(std::memory_order_release)
C++ 的 double-checked locking 真正难的不是“怎么写”,而是“怎么证明它没漏掉任意一个重排路径”。哪怕只错一个 memory_order,或漏一次 fence,在高并发下可能几个月才复现一次崩溃——所以除非有特殊约束(比如不能用 C++11),否则别手写。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











