双检锁(Double-Checked Locking,DCL)在早期的 C++ 实现中存在问题,但在 C++11 及以后的标准中可以通过适当的处理来解决这些问题。下面详细分析双检锁存在的问题以及对应的解决办法。
早期双检锁存在的问题
指令重排序问题
在早期的 C++ 中,编译器和处理器为了提高性能,会对指令进行重排序。在双检锁实现单例模式时,创建对象的操作 instance = new Singleton();
可以分解为以下三个步骤:
- 分配内存:为
Singleton
对象分配内存空间。 - 调用构造函数:在分配的内存空间上调用
Singleton
的构造函数来初始化对象。 - 将内存地址赋值给指针:将分配的内存地址赋值给
instance
指针。
然而,编译器和处理器可能会对这三个步骤进行重排序,比如将步骤 2 和步骤 3 的顺序交换,即先将内存地址赋值给 instance
指针,再调用构造函数。这样在多线程环境下,可能会出现以下情况:
- 线程 A 进入
getInstance()
函数,发现instance
为nullptr
,于是加锁并开始创建对象。由于指令重排序,instance
指针先被赋值了内存地址,但对象还未完成初始化。 - 此时线程 B 进入
getInstance()
函数,第一次检查发现instance
不为nullptr
,就直接返回了instance
指针。但实际上对象还未完成初始化,线程 B 使用这个未初始化的对象可能会导致程序崩溃或产生未定义行为。
以下是早期双检锁存在问题的代码示例:
#include <iostream>
#include <mutex>
class Singleton {
private:
Singleton() {}
static Singleton* instance;
static std::mutex mtx;
public:
static Singleton* getInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
if (instance == nullptr) {
instance = new Singleton(); // 可能发生指令重排序
}
}
return instance;
}
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
C++11 及以后的解决办法
使用 std::atomic
在 C++11 及以后的标准中,引入了原子类型 std::atomic
,可以保证原子操作,避免指令重排序问题。以下是使用 std::atomic
实现的双检锁单例模式:
#include <iostream>
#include <mutex>
#include <atomic>
class Singleton {
private:
Singleton() {}
static std::atomic<Singleton*> instance;
static std::mutex mtx;
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(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 Singleton();
std::atomic_thread_fence(std::memory_order_release);
instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
};
std::atomic<Singleton*> Singleton::instance(nullptr);
std::mutex Singleton::mtx;
在上述代码中,使用 std::atomic
类型的 instance
来保证原子操作,同时使用 std::atomic_thread_fence
来插入内存屏障,防止指令重排序,确保对象在赋值给 instance
指针之前已经完成初始化。
使用 std::call_once
另一种更简洁的解决办法是使用 std::call_once
,它可以保证某个函数在多线程环境下只被调用一次。以下是使用 std::call_once
实现的单例模式:
#include <iostream>
#include <mutex>
class Singleton {
private:
Singleton() {}
static Singleton* instance;
static std::once_flag flag;
static void init() {
instance = new Singleton();
}
public:
static Singleton* getInstance() {
std::call_once(flag, init);
return instance;
}
};
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::flag;
在上述代码中,std::call_once
会保证 init
函数只被调用一次,从而确保单例对象只被创建一次,避免了双检锁的指令重排序问题。
综上所述,早期的双检锁存在指令重排序问题,但在 C++11 及以后的标准中,可以通过使用 std::atomic
或 std::call_once
来解决这些问题,实现线程安全的单例模式。