C++11前双重检查锁定易出问题,主因是编译器/CPU重排序致instance指针提前赋值而对象未构造完,volatile无效且缺乏标准内存模型;C++11起推荐局部静态变量实现,线程安全、异常安全、简洁高效。

为什么双重检查锁定在C++11之前容易出问题
核心原因是编译器重排序和CPU指令重排可能导致 instance 指针被提前赋值,而对象构造尚未完成。其他线程看到非空指针后直接使用,就会访问未初始化的内存,引发未定义行为。
即使加了 std::mutex,若不配合内存序控制,也无法阻止这种重排。C++11 之前缺乏标准的内存模型支持,volatile 在此场景下完全无效——它既不能禁止重排,也不能提供跨线程同步语义。
- 老式写法中用
volatile Singleton* instance是典型误区 - 没有
std::atomic_thread_fence或原子操作保障,if (instance == nullptr)的两次检查之间无同步依据 - 构造函数抛异常时,
instance可能已置为非空但对象未就绪,后续调用会崩溃
C++11 及以后推荐用局部静态变量(最简且安全)
这是目前最推荐的方式:利用 C++11 标准保证的“函数内局部静态变量的首次初始化是线程安全的”,由编译器自动插入必要的锁和内存屏障,无需手动管理。
它天然规避了双重检查的所有陷阱,代码简洁,性能好(仅首次调用有开销),且支持异常安全(初始化失败时下次仍会重试)。
立即学习“C++免费学习笔记(深入)”;
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // ✅ 线程安全,延迟初始化
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
- 注意:必须是
static Singleton instance;,不是static Singleton* instance = new Singleton; - 该机制依赖编译器实现(如 GCC/Clang/MSVC 均已完整支持),无需额外标志
- 如果类构造函数可能抛异常,标准规定:每次调用
getInstance()都会重新尝试初始化,直到成功或程序终止
如果非要手写双重检查锁定,请严格按 C++11+ 规范来
关键点不在“双重检查”,而在原子操作与内存序的精确控制。必须使用 std::atomic 和显式 memory_order,否则仍是错的。
class Singleton {
public:
static Singleton& getInstance() {
Singleton* ptr = instance.load(std::memory_order_acquire);
if (ptr == nullptr) {
std::lock_guard lock(mtx);
ptr = instance.load(std::memory_order_relaxed);
if (ptr == nullptr) {
ptr = new Singleton();
instance.store(ptr, std::memory_order_release);
}
}
return *ptr;
}
private:
static std::atomic instance;
static std::mutex mtx;
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
std::atomic Singleton::instance{nullptr};
std::mutex Singleton::mtx;
-
instance 必须是 std::atomic,不能是裸指针 + volatile
- 首次读用
memory_order_acquire,写用 memory_order_release,确保构造完成对其他线程可见
- 内部第二次读可用
memory_order_relaxed,因已持锁,无需额外同步
- 仍需注意:析构无法自动管理;若需销毁逻辑,得额外设计(如
atexit 或手动清理)
局部静态变量方式的隐藏限制你可能忽略
它虽简单可靠,但有两个实际约束常被忽视:
- 无法控制销毁时机:对象在 main() 返回后、全局对象析构阶段被销毁,若其他静态对象的析构函数中调用
getInstance(),可能触发二次初始化或访问已销毁对象
- 不适用于需要自定义内存分配(如 placement new)或跨 DLL 边界的场景:各模块可能拥有独立的局部静态变量实例
- 若单例依赖其他尚未初始化的静态对象(比如某全局日志器),构造顺序不可控,可能 crash
遇到这些情况,才值得考虑带控制权的双重检查实现,但务必用原子操作,别碰 volatile 或手写汇编屏障。









