双重检查锁在C++11前不安全,因缺乏内存模型定义,new操作可能重排导致指针提前赋值而构造未完成,引发未定义行为;C++11后需用std::atomic配合acquire/release序正确实现。

为什么双重检查锁在 C++11 之前不安全
因为早期 C++ 标准没有明确定义内存模型,new 操作可能被编译器或 CPU 重排:对象内存分配完成、但构造函数还没执行完时,instance 指针已被赋值。其他线程看到非空指针就直接返回,结果用到未初始化的对象——崩溃或未定义行为。
常见错误现象:Segmentation fault、随机字段为零、析构时 double-free,尤其在高并发下偶发出现,极难复现。
- C++11 起,
std::atomic+memory_order_acquire/memory_order_release才能真正约束重排 - 不要用
volatile替代原子操作——它不阻止指令重排,也不能保证跨线程可见性 - 老项目若还在用
pthread_once或全局静态对象,其实是更稳妥的选择
如何用 std::atomic 正确实现 DCL(C++11+)
核心是把单例指针声明为 std::atomic<Singleton*>,并严格控制读写内存序。构造必须在临界区内完成,且首次写入要用 memory_order_release,后续读取用 memory_order_acquire。
class Singleton {
public:
static Singleton* getInstance() {
Singleton* ptr = instance.load(std::memory_order_acquire);
if (ptr == nullptr) {
std::lock_guard<std::mutex> lock(mutex);
ptr = instance.load(std::memory_order_relaxed);
if (ptr == nullptr) {
ptr = new Singleton();
instance.store(ptr, std::memory_order_release);
}
}
return ptr;
}
<p>private:
Singleton() = default;
static std::atomic<Singleton*> instance;
static std::mutex mutex;
};</p><p>std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mutex;-
load(std::memory_order_relaxed)在加锁后二次检查,避免无谓的 acquire 开销 - 构造
new Singleton()必须在std::lock_guard保护内,否则多个线程可能同时进入并重复构造 - 不能把
instance.store()放在lock外——会破坏 release 语义,导致其他线程看到指针但看不到构造结果
比 DCL 更简单且线程安全的替代方案
C++11 起,局部静态变量的初始化天然线程安全,由编译器保证“首次访问时原子地构造一次”。它比手写 DCL 更短、更可靠,且无性能损失(现代编译器已优化为类似 DCL 的汇编)。
立即学习“C++免费学习笔记(深入)”;
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
private:
Singleton() = default;
};- 无需手动管理指针、内存或锁,也不会有内存泄漏风险(静态对象生命周期由程序控制)
- 如果需要延迟初始化但又必须返回指针(而非引用),可包一层
static auto& inst = *new Singleton(),但注意:这绕过了自动析构,需自行管理 - 某些嵌入式环境或禁用 RTTI/异常的构建中,静态初始化可能被禁用,此时才需退回原子 DCL
容易被忽略的析构与销毁顺序问题
无论是 DCL 还是局部静态,单例对象的析构时机都不可控:它在 main() 返回后、全局对象析构阶段执行,此时其他静态对象可能已被销毁。若单例析构函数里调用了另一个单例,就会 crash。
- 不要在单例析构函数中依赖任何其他全局或静态资源(包括日志、配置、线程池等)
- 如果必须控制销毁顺序,改用显式
init()/shutdown(),让调用方决定何时释放资源 - 局部静态方案无法手动销毁;DCL 方案可通过
delete instance.exchange(nullptr)强制释放,但必须确保所有线程已停止访问
真正麻烦的从来不是“怎么创建”,而是“谁来管它什么时候消失”——尤其当单例持有线程、文件句柄或网络连接时,这点比线程安全更难测试和验证。










