std::call_once比手写双重检查更安全,因其由标准库保证仅执行一次、原子性及自动内存屏障,避免重排序、裸指针竞态、volatile误用及异常安全等问题。

为什么直接用 std::call_once 比手写双重检查更安全
双重检查锁定(DCLP)在 c++11 之前是单例线程安全初始化的“权宜之计”,但容易因内存重排序、编译器优化或缺少内存屏障而崩溃——比如构造函数还没执行完,另一个线程就拿到了未初始化的 instance 指针并开始调用成员函数。
现代 C++(C++11 起)提供了更简单、无歧义的替代方案:std::call_once + std::once_flag。它由标准库保证:仅执行一次、原子性、自动插入必要内存屏障,且不依赖指针状态判断。
-
std::call_once是语言级保障,无需手动管理锁、判断、释放等逻辑 - 避免了
volatile的误用(它在 C++ 中对 DCLP 完全无效) - 不涉及原始指针生命周期管理,规避
new失败后未清理、析构时机不确定等问题
手写双重检查锁定的典型错误写法
下面这段代码看似合理,实则危险:
if (instance == nullptr) { std::lock_guard<std::mutex> lock(mutex_); if (instance == nullptr) { instance = new Singleton(); // ❌ 构造可能被重排到赋值之后 } }
问题不止在构造顺序:C++ 标准不保证 new 分配内存 → 调用构造函数 → 写入 instance 这三步的可见性顺序。其他线程可能看到非空的 instance,但其内部成员仍是垃圾值。
立即学习“C++免费学习笔记(深入)”;
- 漏掉
std::atomic<singleton></singleton>声明,导致读写非原子,触发未定义行为 - 用
volatile Singleton*替代原子操作,完全无效(volatile不提供同步语义) - 没用
memory_order_acquire/release,即使用了std::atomic,默认memory_order_seq_cst也常被误简化
如果真要手写 DCLP,必须满足哪些条件
仅当目标平台/编译器明确支持、且你已充分理解内存模型时才考虑。否则请用 std::call_once。
-
instance必须声明为std::atomic<singleton></singleton>,不能是裸指针或volatile - 第一次读取用
.load(std::memory_order_acquire),第二次写入用.store(ptr, std::memory_order_release) - 构造必须在临界区内完成,且确保异常安全(例如用
std::unique_ptr管理中间对象) - 所有访问
instance的地方都需用原子读取(不能直接解引用裸指针)
示例关键片段:
static std::atomic<Singleton*> instance{nullptr}; // ... Singleton* ptr = instance.load(std::memory_order_acquire); if (ptr == nullptr) { std::lock_guard<std::mutex> lock(mutex_); ptr = instance.load(std::memory_order_relaxed); if (ptr == nullptr) { ptr = new Singleton(); // 构造完成后才 store instance.store(ptr, std::memory_order_release); } }
静态局部变量方案其实更推荐
C++11 规定:函数内静态局部变量的初始化是线程安全的,且仅发生一次。它底层通常就用 std::call_once 实现,但语法干净、无出错空间。
- 无需显式锁、原子变量、内存序,也不用担心
delete或生命周期 - 初始化失败(抛异常)会自动重试下次调用,符合预期
- 比手写 DCLP 更快(无原子读开销),比
std::call_once更少样板代码
写法就是:
static Singleton& getInstance() { static Singleton instance; // ✅ 线程安全、懒加载、自动销毁 return instance; }
注意:这个 instance 是对象本身,不是指针;若需指针,返回 &instance 即可。
真正难的是让所有人相信——最简单的写法,恰恰是最正确、最可维护、最容易通过 Code Review 的写法。别在单例上赌自己比标准库更懂并发。