最稳妥的线程安全单例写法是 std::call_once 配合局部静态变量,它规避了双重检查锁定的内存序陷阱,利用 C++11 对局部静态变量初始化的线程安全保证,且比手写锁更轻量。

std::call_once + std::once_flag 是目前最稳妥的线程安全单例写法
直接用 std::call_once 配合局部静态变量,能避开双重检查锁定(DCLP)的手动同步陷阱,也比手写锁更轻量。C++11 起标准保证局部静态变量的初始化是线程安全的,但很多人误以为“只要 static 就自动线程安全”,其实仅限于**首次初始化过程**——后续调用不加锁没问题,但初始化阶段必须靠 std::call_once 或语言机制兜底。
常见错误现象:std::mutex 手动加锁漏释放、pthread_once 混用 C/C++ ABI、或在构造函数里调用虚函数导致未定义行为。
- 必须把单例对象声明为函数内局部静态变量,不能是类静态成员(否则初始化时机不可控)
-
std::once_flag必须是静态或全局生命周期,不能是局部栈变量(否则每次调用都新建,失效) - 不要在
getInstance()里做耗时操作(比如文件读取、网络请求),否则会阻塞所有等待线程
为什么 double-checked locking 在 C++ 里容易出错
不是语法错,而是内存序和编译器重排导致的逻辑漏洞。即使加了 std::mutex 和 if (ptr == nullptr) 两层检查,若没用 std::atomic 修饰指针、没加 memory_order_acquire/release,就可能让其他线程看到未构造完成的对象地址。
典型错误现象:程序偶发崩溃在单例对象的某个成员函数第一行,gdb 显示该对象部分字段为零值或垃圾值,但指针非空。
立即学习“C++免费学习笔记(深入)”;
- 必须用
std::atomic<t></t>存储原始指针,不能用裸指针 +volatile(C++ 中volatile不提供线程同步语义) - 第一次检查要用
load(std::memory_order_acquire),赋值要用store(ptr, std::memory_order_release) - 构造函数不能抛异常,否则
delete不会被执行,且std::atomic指针不会回滚
懒汉式 vs 饿汉式:选哪个取决于初始化成本和使用确定性
饿汉式(全局静态对象)启动即构造,天然线程安全,但浪费资源;懒汉式按需创建,省资源但引入同步开销。二者没有绝对优劣,只看场景。
使用场景举例:日志模块可饿汉(必须早于 main 初始化),配置管理器应懒汉(可能根本不用)。
- 饿汉式必须确保构造函数不依赖其他未初始化的全局对象(避免静态初始化顺序问题,SIOF)
- 懒汉式若用局部静态变量实现,C++11 后无需额外同步,是最简方案;若用指针+new,则必须配
std::call_once或std::atomic - Windows DLL 场景下,饿汉式可能触发 DLL_THREAD_ATTACH 失败,懒汉式更可控
std::shared_ptr 能不能用来实现线程安全单例
可以,但不推荐。用 std::shared_ptr 管理单例生命周期看似方便,实则引入原子引用计数开销,且无法阻止用户意外拷贝出多个 std::shared_ptr 实例,破坏“单例”语义。
常见错误现象:代码里出现两个不同地址的“同一个单例”,表现为状态不一致、资源重复释放。
- 若坚持用
std::shared_ptr,必须把构造函数设为私有,并禁用拷贝,只允许通过getInstance()返回 const 引用或std::shared_ptr的 const 版本 -
std::shared_ptr的线程安全性仅限于“多个线程同时读/写不同实例”,对同一实例的operator=或reset()仍需外部同步 - 真正需要共享所有权的场景,往往已不该叫“单例”,而应叫“全局服务对象”,此时考虑依赖注入更合适
最易被忽略的一点:单例的析构时机。C++ 标准不保证静态对象析构顺序,若单例析构时访问了已被销毁的其他静态对象,就会 crash。别依赖“反正程序快结束了”,尤其在测试框架或插件系统里,这个问题会反复出现。











