C++11起static局部变量是懒汉单例首选,因其首次初始化线程安全、编译器自动生成同步逻辑、无需手动加锁、避免堆分配且构造仅执行一次。

直接用 static 局部变量实现延迟初始化,是 C++11 起最安全、最简洁的做法;手写双重检查锁(DCLP)或裸指针 + std::mutex 容易出错,不推荐作为首选。
为什么 static 局部变量是懒汉式首选
C++11 标准规定:函数内 static 局部变量的首次初始化是线程安全的,由编译器自动生成同步逻辑(如 GCC 用 __cxa_guard_acquire),无需手动加锁。这比所有手写同步方案都更轻量、更可靠。
- 构造函数只执行一次,哪怕多线程并发调用
getInstance() - 析构时机由程序结束时静态对象生命周期规则决定,无需担心提前释放
- 不依赖
new/delete,避免堆分配开销和内存碎片 - 编译器若禁用了该特性(如 GCC 加了
-fno-threadsafe-statics),链接会失败,问题暴露得早
std::call_once + std::once_flag 适合带参数或需控制时机的场景
当单例构造需要读配置、传参数,或你希望把初始化推迟到某个明确的启动阶段(比如插件加载后),std::call_once 比 static 局部变量更可控。
-
std::once_flag必须是静态存储期——不能是函数栈上临时变量,否则每次调用都新建一个,失去“只一次”语义 - 回调函数里严禁抛异常,否则程序可能终止(C++ 标准未定义行为)
- 不要在初始化回调里调用另一个同样用
std::call_once的函数,否则可能死锁 - 示例中用
std::shared_ptr配合自定义deleter,是为了支持显式销毁;若不需要重置,直接用static std::unique_ptr更轻量
手写双重检查锁(DCLP)为什么容易翻车
看似高效,实则对内存序、原子操作、编译器重排极其敏感,99% 的手写版本存在隐患。
立即学习“C++免费学习笔记(深入)”;
- 漏掉
volatile或用错std::atomic类型(比如用int存指针),会导致多核下看到未完全构造的对象 - 没用
std::memory_order_acquire/release控制顺序,构造函数内的写操作可能被重排到指针赋值之后 - 在 DLL/so 中使用时,Windows 下 DLL 延迟加载可能导致
static成员未初始化就进入临界区 - 饿汉式虽线程安全,但无法规避“未使用却初始化”的资源浪费,且不支持运行时重载
显式销毁与重初始化的代价常被低估
标准 static 局部变量方案天然不支持运行时销毁。一旦选择手动管理(如 std::atomic<T*> + std::mutex),你就得自己处理所有访问同步、生命周期边界、use-after-free 和析构死锁问题。
- 销毁接口(如
resetInstance())必须确保所有线程已退出对该单例的任何引用,否则 crash 是大概率事件 - 不要在析构函数里调用
getInstance(),极易触发静态初始化顺序问题或递归锁 - 单元测试中频繁重置?优先考虑用依赖注入替代全局单例,而不是硬刚销毁逻辑


















