必须用 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
注意:load(std::memory_order_relaxed) 在加锁后第二次读是安全的,因为锁本身提供了同步;而首次读必须用 acquire,确保后续读能看到 release 写入的完整对象状态。
最容易被忽略的坑:静态局部变量不是万能解
C++11 规定静态局部变量的初始化是线程安全的(即所谓“Meyers 单例”),但它不等于懒汉+双重检查锁——它本质是编译器插入了类似 std::call_once 的逻辑,且无法控制构造时机以外的行为(比如无法在构造前做资源预检、无法定制错误处理路径)。
更隐蔽的问题是:如果单例构造函数抛异常,静态局部变量版本会每次调用都重试构造,而双重检查锁版本一旦失败,instance_ 仍为 nullptr,下次调用可重新尝试(需你自己加异常捕获逻辑)。
- 静态局部变量够用?优先用它。它简洁、标准、无内存序风险
- 需要构造失败后重试、或构造前 hook、或严格控制对象生命周期(比如配合
atexit销毁)?才值得上双重检查锁 - 别以为把
static Singleton s;写在函数里就自动获得高性能——它的首次调用开销和std::call_once基本一致


















