C++中的双重检查锁定(Double-Checked Locking)是什么?(如何实现单例)

1次阅读

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

C++中的双重检查锁定(Double-Checked Locking)是什么?(如何实现单例)

为什么直接用 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 的写法。别在单例上赌自己比标准库更懂并发

text=ZqhQzanResources