这个模式主要用于“懒汉式”初始化单例,结构如下:
std::atomic<MySingleton*> instance = nullptr;
std::mutex mtx;
MySingleton* getInstance() {
MySingleton* tmp = instance.load(std::memory_order_acquire); // 第一次检查(无锁)
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed); // 第二次检查(加锁后)
if (tmp == nullptr) {
tmp = new MySingleton(); // 初始化
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
atomic.load()
真能拦住多线程吗?不能完全拦住多线程同时进入 if 判断,但它是有意义的。
真正的防止并发初始化,靠的是加锁 + 第二次判断(内层 check)。
外层 load()
是一种“快速路径”优化:
load()
是非阻塞的、快速的:它会拦住大部分非首次访问的线程,避免加锁;if (tmp == nullptr)
是完全可能的,除非你加锁。所以外层
atomic.load()
不是“拦住线程”,而是:
- 提前判断,避免争抢锁;
- 减少后续加锁压力;
- 真正的并发安全靠 mutex + 第二次检查。
层级 | 操作 | 能力 |
---|---|---|
外层 atomic.load | 快速判断 | ❌ 不能完全阻止并发初始化 |
mutex + 内层检查 | 严格保护 | ✅ 保证只有一个线程初始化 |
store(release) / load(acquire) | 保证内存可见性 | ✅ 保证其他线程看到完整对象 |
C++11 之后 DCL 是安全的,但要求:
instance
是 std::atomic
。memory_order_acquire
/ release
保证内存顺序。DCL 的历史 bug(构造未完成)
如果你写的是:
MySingleton* tmp = new MySingleton(); // tmp 构造中
instance.store(tmp); // 此时 store 了一个尚未完整构造的对象!
另一个线程可能这样:
MySingleton* p = instance.load(); // 获取到指针
p->doSomething(); // 使用未初始化完的对象!
所以 C++11 要求:用 store(memory_order_release)
+ load(memory_order_acquire)
保证:
store
之前的所有写(对象构造)对 load
之后可见;在现代 C++ 中,推荐更安全的方式:
使用 std::call_once
+ std::once_flag
std::once_flag flag;
MySingleton* instance = nullptr;
MySingleton* getInstance() {
std::call_once(flag, []() {
instance = new MySingleton(); // 构造只会被调用一次
});
return instance;
}
这是最安全、最简洁的单例方式。
atomic.load()
并不能“拦住多线程”,它只是优化路径。std::call_once
是更现代的、安全的替代方案。