饿汉式天生线程安全,因静态实例在main前由单线程完成初始化;懒汉式默认不安全,需双重检查锁(dcl)配合atomic/volatile防重排,仅首次创建同步。

饿汉式天然线程安全,懒汉式默认不安全;多线程下直接用饿汉最省心,非要懒加载就得加双重检查锁(DCL),且必须用 std::atomic 或 volatile 防止指令重排。
饿汉式为什么天生线程安全
因为 static 全局对象的初始化发生在 main() 之前,由单线程(程序启动时的初始化阶段)完成。所有线程看到的 m_instance 都是已构造完毕的同一块内存。
- 无需任何锁,无性能开销
- 构造函数里做耗时操作(如读配置、连数据库)会拖慢程序启动,但不会引发竞态
- 静态成员变量必须在类外定义并初始化,例如:
Singleton Singleton::m_instance; - 如果构造函数抛异常,程序会直接终止(C++ 标准规定静态初始化失败即
std::terminate)
懒汉式不加锁一定出问题
典型错误现象是:两个线程同时执行 if (p == nullptr) 判断为真,各自调用 new Singleton,最终生成两个实例——彻底违反单例语义。
-
new Singleton不是原子操作:分配内存 + 调用构造函数 + 写入指针,三步可能被重排 - 即使加了互斥锁,每次调用
GetInstance()都锁,性能差(尤其高并发场景) - 只在首次创建时需要同步,后续应直接返回指针
懒汉式线程安全的正确写法(DCL)
双重检查锁(Double-Checked Locking)是主流解法,但 C++11 以前极易出错;C++11 起必须配合 std::atomic 保证可见性与顺序性。
- 指针变量必须声明为
static std::atomic<singleton> p{nullptr}</singleton>,不能用裸指针 - 第一次检查用
load(std::memory_order_acquire),避免编译器/处理器重排构造动作 - 加锁后第二次检查不可省略,防止多个线程在锁外都通过第一层判断
- 构造完成后用
store(p, std::memory_order_release)确保构造完成对其他线程可见 - 示例关键片段:
static std::atomic<singleton> p{nullptr}; Singleton* Singleton::GetInstance() { Singleton* tmp = p.load(std::memory_order_acquire); if (tmp == nullptr) { std::lock_guard<:mutex> lock(mutex_); tmp = p.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new Singleton(); p.store(tmp, std::memory_order_release); } } return tmp; }</:mutex></singleton>
饿汉 vs 懒汉:选哪个取决于初始化时机和资源代价
饿汉强制提前初始化,懒汉延迟但需小心线程模型;没有绝对优劣,只有是否匹配场景。
- 配置管理、日志器、全局缓存等「启动即需」组件,优先饿汉——简单、安全、零风险
- 数据库连接池、大内存缓冲区等「按需创建且初始化昂贵」的组件,才考虑带 DCL 的懒汉
- 注意:C++11 起推荐用局部静态变量实现懒汉(
static Singleton instance;),由标准保证线程安全,但仅适用于无参构造且不需在析构时做清理的场景 - 析构顺序不可控:饿汉实例在
main结束后销毁,若其他静态对象依赖它,可能访问已析构对象
真正容易被忽略的是:懒汉的 DCL 实现中,std::atomic 的 memory order 选错或漏掉,会导致极难复现的偶发多实例;而饿汉的构造异常会静默终止程序——这两点在线上环境都极难定位。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











