c++11前双检锁不可靠,因编译器重排和cpu乱序执行致对象未构造完成就被指针赋值;c++11起推荐magic Static,因其线程安全、零出错且无性能损耗。

双检锁在C++11之前为什么不可靠
因为编译器重排和CPU乱序执行,new Singleton()可能被拆成“分配内存→写入对象→赋值给静态指针”三步,而第二步和第三步顺序可能被交换。线程A执行到一半,线程B就看到非空指针但实际对象未构造完成,直接访问会崩溃。
即使加了volatile(老式写法),也不能阻止编译器对指针本身的重排,C++11前没有可靠的内存屏障语义。
- 不要在C++11之前项目里手写双检锁单例
- 如果必须兼容旧标准,用pthread_once或windows的InitOnceExecuteOnce
- C++11起,
std::atomic+ 显式memory_order能修复,但没必要——有更简单的方案
为什么magic static是首选方案
C++11明确规定:函数内静态局部变量的首次初始化是线程安全的,且仅发生一次。编译器自动插入必要的锁和内存栅栏,无需手动干预。
它比双检锁更短、更易读、零出错率,且无性能损耗(只在首次调用时有轻微开销,之后完全无锁)。
立即学习“C++免费学习笔记(深入)”;
class Singleton { public: static Singleton& instance() { static Singleton inst; // ✅ magic static return inst; } private: Singleton() = default; };
- 必须是函数作用域内的
static变量,类内static成员不享受此保证 - 构造函数不能抛异常;一旦抛出,下次调用仍会尝试构造,可能重复抛出
- 析构时机由实现定义,但保证在main返回后、全局对象析构前完成
双检锁在C++11+下还能用吗
能,但属于“可工作但不推荐”的方案。它需要正确使用std::atomic和memory_order_acquire/release,稍有不慎就退化为未定义行为。
典型错误包括:用int*代替std::atomic<singleton></singleton>、漏掉atomic_thread_fence、或在构造完成前就用store写入指针。
- 若坚持双检锁,必须用
std::atomic<singleton></singleton>存储指针 -
load用memory_order_acquire,store用memory_order_release - 构造对象必须在
store之前完成,且不能被编译器移到store之后(需atomic_thread_fence(memory_order_release)或等效手段)
静态变量生命周期与DLL/so场景的坑
magic static在单个模块内绝对安全,但在跨动态库边界时,不同DLL可能各自初始化一份static变量——这不是线程问题,而是ODR(One Definition Rule)违反。
例如Windows下两个DLL都定义了同名Singleton::instance(),主程序链接时可能各用各的实例,导致“单例”变成“多例”。
- 跨模块共享单例,必须确保该函数符号全局唯一(如导出为
__declspec(dllexport)并显式导入) - linux下注意
-fvisibility=hidden可能让符号无法跨so可见 - 最稳妥做法:单例逻辑只实现在主程序或一个明确的core库中,其他模块通过接口访问
C++11起,magic static就是线程安全单例的事实标准。真正要注意的不是“怎么写”,而是“在哪定义”和“异常是否可控”——这两点比锁策略更容易出问题。