c++如何实现单例模式_c++线程安全单例模式写法【面试】

2次阅读

c++11 之前 double-checked locking 不安全,因编译器和 CPU 重排序可能导致 instance 指针提前赋值而对象未构造完成;C++11 起推荐用 Static 局部变量实现单例,由编译器保证线程安全初始化。

c++如何实现单例模式_c++线程安全单例模式写法【面试】

为什么 double-checked locking 在 C++11 之前不安全

因为编译器重排序和 CPU 指令重排,可能导致 instance 指针被提前赋值,但对象构造尚未完成。线程 A 执行到 if (instance == nullptr) 时看到非空指针,就直接返回未初始化完毕的对象,引发未定义行为。

在 C++11 之前,必须用全局锁(如 pthread_mutex_t)包裹整个获取逻辑,性能差;C++11 引入内存模型后,才真正支持高效且安全的双重检查锁定。

C++11 推荐写法:使用 std::call_once + static 局部变量

这是最简洁、最安全、且天然线程安全的实现方式,由编译器保证初始化仅执行一次,无需手动加锁或检查。

class Singleton { public:     static Singleton& getInstance() {         static Singleton instance; // C++11 起,static 局部变量初始化是线程安全的         return instance;     }     Singleton(const Singleton&) = delete;     Singleton& operator=(const Singleton&) = delete; 

private: Singleton() = default; // 可加初始化逻辑 };

  • 所有主流编译器(GCC、Clang、MSVC)都已正确实现该语义
  • 首次调用 getInstance() 时才构造对象,满足懒加载
  • 无需 std::mutexstd::atomic,无额外开销
  • 析构由程序退出前自动触发,顺序可控(静态对象生命周期)

如果非要手写 double-checked locking:必须用 std::atomicmemory_order

手动实现需严格控制内存序,否则仍可能出错。关键点不是“加锁”,而是防止重排破坏构造顺序。

立即学习C++免费学习笔记(深入)”;

class Singleton { public:     static Singleton* getInstance() {         Singleton* tmp = instance.load(std::memory_order_acquire);         if (tmp == nullptr) {             std::lock_guard lock(mutex_);             tmp = instance.load(std::memory_order_relaxed);             if (tmp == nullptr) {                 tmp = new Singleton();                 instance.store(tmp, std::memory_order_release);             }         }         return tmp;     } 

private: static std::atomic> instance; static std::mutex mutex_; Singleton() = default; }; std::atomic> Singleton::instance{nullptr}; std::mutex Singleton::mutex_;

  • instance 必须是 std::atomic,不能是裸指针
  • 第一次 load 用 memory_order_acquire,确保后续读取不会重排到它前面
  • store 用 memory_order_release,配合 acquire 实现同步点
  • 内部二次 check 是必须的:防止多个线程同时通过第一层 if 后竞争构造
  • 实际项目中几乎没必要这么写,static 局部变量更可靠

面试时最容易被追问的坑

面试官常会问:“如果 Singleton 构造函数抛异常,会怎样?”——答案是:static 局部变量版本会捕获异常,且下次调用仍会尝试构造(C++11 标准规定:若初始化抛异常,该变量视为未初始化,下次进入函数时重试)。

  • 这意味着构造函数必须能处理重复调用的副作用(比如文件打开失败、网络连接失败等)
  • 而手写 double-checked 版本若在 new Singleton() 后、store 前抛异常,会导致 instance 保持为 nullptr,下次调用可重试,行为一致
  • 但若在构造函数中发生资源泄漏(比如 malloc 成功但后续失败),静态局部变量版无法自动回滚,需自行确保构造函数强异常安全

真正难的不是写出来,而是说清“为什么这个 static 变量能线程安全”——本质是编译器生成了类似 std::call_once 的保护代码,背后依赖的是 C++11 的 one-time initialization 机制。

text=ZqhQzanResources