最稳妥的线程安全单例方案是c++11起使用局部静态变量初始化,编译器自动保证只执行一次且线程安全;需禁用所有构造与赋值操作(含移动),并注意构造函数不可抛异常。

单例的线程安全怎么保证
多线程环境下直接用懒汉式(首次调用时初始化)会出问题:两个线程同时判断 instance == nullptr,都进到 new 分支,构造两次,析构时崩溃或内存泄漏。
最稳妥的做法是用 C++11 起支持的局部静态变量初始化,编译器自动加锁且只执行一次:
class Singleton { public: static Singleton& getInstance() { static Singleton instance; // ✅ 线程安全,无需手动同步 return instance; } private: Singleton() = default; ~Singleton() = default; Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; };
- 别手写
std::mutex+double-checked locking:容易漏锁、内存重排,C++11 前才考虑它 - 别用
std::call_once+std::once_flag:可行但冗余,不如局部静态简洁 - 注意:这个方案要求构造函数不能抛异常,否则第二次调用会直接 abort
为什么不能用全局对象替代单例
全局对象在 main() 之前初始化,但它的初始化顺序跨编译单元是未定义的。如果 A.cpp 的全局对象依赖 B.cpp 的单例,而 B 的单例还没构造,就会访问野指针。
局部静态变量则不同:它的初始化发生在第一次调用函数时,时机可控,依赖关系明确。
立即学习“C++免费学习笔记(深入)”;
- 全局对象还可能被链接器优化掉(尤其没显式引用时),导致运行时报
undefined reference - 单例可延迟初始化,节省启动时间;全局对象不管用不用,一加载就构造
- 单例能控制析构顺序(比如在 main 结束后、全局对象析构前清理资源),全局对象做不到
delete 构造函数和赋值操作符要写全
只禁用拷贝构造、不删移动构造,C++11 后仍可能被移动构造出新对象,破坏唯一性。
必须显式删除所有可能绕过单例控制的入口:
private: Singleton() = default; ~Singleton() = default; Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; Singleton(Singleton&&) = delete; Singleton& operator=(Singleton&&) = delete;
- 漏删移动语义是常见疏忽,尤其在类有资源管理逻辑时
- 构造函数设为
private但没删拷贝/移动,编译器会自动生成默认版本,等于没封住 - 如果真需要“可移动的单例”,说明设计本身有问题——单例的本质就是全局唯一,不该被转移
单例生命周期比 main 长怎么办
局部静态变量的析构发生在 main 返回后、全局对象析构期间,顺序不可控。如果单例里持有日志句柄、网络连接等资源,可能在依赖它的其他全局对象析构时已失效。
更可控的方式是用裸指针 + 显式销毁,把控制权交还给使用者:
class Singleton { public: static Singleton* getInstance() { if (!instance) { instance = new Singleton(); } return instance; } static void destroy() { delete instance; instance = nullptr; } private: static Singleton* instance; };
- 这种模式叫“manual lifetime management”,适合嵌入式或游戏引擎等对析构顺序敏感的场景
- 必须确保
destroy()在所有依赖它的对象析构前调用,通常放在 main 末尾或 atexit 回调里 - 别忘了加
thread_local或锁保护instance指针读写,否则多线程下仍不安全
真正难的从来不是写出来,而是想清楚:这个“唯一实例”到底该活多久、谁来决定它什么时候死、以及有没有人会在它死后还试图访问它。