C++11 之前 double-checked locking 不安全,因编译器和 CPU 重排序可能导致 instance 指针提前赋值而对象未构造完成;C++11 起推荐用 static 局部变量实现单例,由编译器保证线程安全初始化。

为什么 double-checked locking 在 C++11 之前不安全
因为编译器重排序和 CPU 指令重排,可能导致 instance 指针被提前赋值,但对象构造尚未完成。线程 A 执行到 if (instance == nullptr) 时看到非空指针,就直接返回未初始化完毕的对象,引发未定义行为。
在 C++11 之前,必须用全局锁(如 pthread_mutex_t)包裹整个获取逻辑,性能差;C++11 引入内存模型后,才真正支持高效且安全的双重检查锁定。
C++11 推荐写法:使用 std::call_once + static 局部变量
这是最简洁、最安全、且天然线程安全的实现方式,由编译器保证初始化仅执行一次,无需手动加锁或检查。
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 起,static 局部变量初始化是线程安全的
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default; // 可加初始化逻辑
};
- 所有主流编译器(GCC、Clang、MSVC)都已正确实现该语义
- 首次调用
getInstance()时才构造对象,满足懒加载 - 无需
std::mutex或std::atomic,无额外开销 - 析构由程序退出前自动触发,顺序可控(静态对象生命周期)
如果非要手写 double-checked locking:必须用 std::atomic 和 memory_order
手动实现需严格控制内存序,否则仍可能出错。关键点不是“加锁”,而是防止重排破坏构造顺序。
立即学习“C++免费学习笔记(深入)”;
class Singleton {
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard lock(mutex_);
tmp = instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
private:
static std::atomic> instance;
static std::mutex mutex_;
Singleton() = default;
};
std::atomic> Singleton::instance{nullptr};
std::mutex Singleton::mutex_;
-
instance必须是std::atomic,不能是裸指针 - 第一次 load 用
memory_order_acquire,确保后续读取不会重排到它前面 - store 用
memory_order_release,配合 acquire 实现同步点 - 内部二次 check 是必须的:防止多个线程同时通过第一层 if 后竞争构造
- 实际项目中几乎没必要这么写,
static局部变量更可靠
面试时最容易被追问的坑
面试官常会问:“如果 Singleton 构造函数抛异常,会怎样?”——答案是:static 局部变量版本会捕获异常,且下次调用仍会尝试构造(C++11 标准规定:若初始化抛异常,该变量视为未初始化,下次进入函数时重试)。
- 这意味着构造函数必须能处理重复调用的副作用(比如文件打开失败、网络连接失败等)
- 而手写 double-checked 版本若在
new Singleton()后、store 前抛异常,会导致instance保持为nullptr,下次调用可重试,行为一致 - 但若在构造函数中发生资源泄漏(比如 malloc 成功但后续失败),静态局部变量版无法自动回滚,需自行确保构造函数强异常安全
真正难的不是写出来,而是说清“为什么这个 static 变量能线程安全”——本质是编译器生成了类似 std::call_once 的保护代码,背后依赖的是 C++11 的 one-time initialization 机制。











