
双重检查锁定为什么必须用 volatile 和 memory_order
不加内存屏障的双重检查锁定在多线程下可能返回未初始化完成的对象。编译器重排和 CPU 乱序执行会导致 instance 指针被提前写入非空值,而构造函数体尚未执行完毕。C++11 起必须用 std::atomic 替代裸指针,并显式指定内存序。
标准写法:std::atomic + memory_order_acquire/release
使用 std::atomic 管理实例指针,配合 memory_order_acquire(读)和 memory_order_release(写),确保构造完成前的所有写操作对其他线程可见。不能用 memory_order_relaxed,否则仍存在重排风险。
class Singleton {
private:
static std::atomic instance;
static std::mutex mtx;
Singleton() = default; // 防止外部构造
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton getInstance() {
Singleton ptr = instance.load(std::memory_order_acquire);
if (ptr == nullptr) {
std::lock_guard<:mutex> lock(mtx);
ptr = instance.load(std::memory_order_acquire);
if (ptr == nullptr) {
ptr = new Singleton();
instance.store(ptr, std::memory_order_release);
}
}
return ptr;
}
};
std::atomic Singleton::instance{nullptr};
std::mutex Singleton::mtx;
为什么不能用 raw pointer + volatile 代替 atomic
volatile 只禁用编译器优化,不阻止 CPU 乱序或提供跨线程同步语义。在 C++ 中它**不是**线程同步机制。常见错误是写成 static volatile Singleton* instance = nullptr;,这无法防止指令重排,也不能保证 store/load 的原子性与可见性。
-
volatile 不触发内存屏障,std::atomic 的 load/store 才能生成对应汇编级 fence 指令
- 裸指针的赋值(如
instance = new Singleton())不是原子操作,可能被拆分为地址写入+构造两步
- 即使加了
volatile,GCC/Clang 仍可能对 surrounding 内存访问做重排
更安全的替代:std::call_once + std::once_flag
如果不需要延迟初始化的极致性能,直接用 std::call_once 是最简洁、无脑安全的方案。它内部已封装内存屏障与互斥逻辑,且支持异常安全(若构造函数抛异常,后续调用仍会重试)。
立即学习“C++免费学习笔记(深入)”;
class Singleton {
private:
static std::unique_ptr instance;
static std::once_flag init_flag;
Singleton() = default;
public:
static Singleton& getInstance() {
std::call_once(init_flag, []() {
instance = std::make_unique();
});
return *instance;
}
};
std::unique_ptr Singleton::instance = nullptr;
std::once_flag Singleton::init_flag;
注意:这里返回引用而非指针,避免空指针解引用风险;std::call_once 保证初始化只执行一次,且所有线程等待完成后再继续——这个“完成”包含完整的构造与内存可见性同步。
真正容易被忽略的是:双重检查锁定不是银弹。它只有在高竞争、低初始化频率场景下才比 std::call_once 有优势;多数业务代码里,后者更可读、更难出错、且现代 libc++/libstdc++ 实现已足够快。











