"双重检查锁定"(Double-Checked Locking)是一种用于提高多线程环境下性能的设计模式,主要用于懒初始化(lazy initialization)场景。它确保了在多线程情况下,某个资源(如单例实例)只被初始化一次,并且在初始化后访问时无需加锁,从而减少不必要的锁开销。
双重检查锁定的工作原理
双重检查锁定的核心思想是,在对某个共享资源进行访问时,首先在锁外进行一次检查,如果不满足条件(例如资源尚未初始化),才在锁内进行第二次检查,并执行初始化操作。这种方式可以避免每次访问资源时都进行加锁操作,降低锁带来的性能开销。
经典的双重锁定示例
以下是一个典型的双重检查锁定模式的实现示例,通常用于单例模式:
cpp
class Singleton {
public:
static Singleton* getInstance() {
if (instance == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_);
if (instance == nullptr) { // 第二次检查
instance = new Singleton(); // 懒初始化
}
}
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance;
static std::mutex mutex_;
};
// 静态成员变量定义
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex_;
工作流程
- 第一次检查: 在 getInstance() 方法中,首先检查 instance 是否为空。如果 instance 已经被初始化,直接返回,不需要加锁。
- 加锁: 如果 instance 为空,表示尚未初始化,此时进入临界区,加锁确保只有一个线程可以执行接下来的初始化代码。
- 第二次检查: 在加锁之后,再次检查 instance 是否为空。这是因为可能有多个线程在第一次检查时同时通过并进入临界区,而此时只有第一个进入临界区的线程需要进行初始化,其他线程需要跳过初始化操作。
- 初始化: 如果 instance 仍为空,执行初始化操作。
- 返回实例: 无论 instance 是否已经初始化,最后都返回该实例。
双重检查锁定存在的问题与解决方案
1、内存模型问题
在某些早期的编译器或平台上,双重检查锁定可能会由于内存模型的原因导致未定义行为。例如,编译器或处理器可能会对代码进行重排序,导致其他线程看到一个部分构造的对象(即,内存已经分配,但构造函数尚未执行完毕),从而产生问题。
2、C++11及以上的解决方案
C++11 引入了更强的内存模型,并提供了 std::atomic 类型和 std::call_once 等工具,帮助开发者更安全地实现懒初始化和双重检查锁定。
- 使用 std::atomic: 可以通过使用 std::atomic 来保证对 instance 的检查是原子的,避免由于编译器或硬件层面的优化导致的重排序问题。
- 使用 std::call_once: C++11 提供的 std::call_once 和 std::once_flag 能够保证某个操作只执行一次,而且是线程安全的,通常可以用来替代双重检查锁定。
cpp
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, []() { instance.reset(new Singleton); });
return *instance;
}
private:
Singleton() = default;
~Singleton() = default;
static std::unique_ptr<Singleton> instance;
static std::once_flag initFlag;
};
// 静态成员变量定义
std::unique_ptr<Singleton> Singleton::instance;
std::once_flag Singleton::initFlag;
在这个实现中,std::call_once 确保 Singleton 的初始化只执行一次,并且是线程安全的。这样可以避免双重检查锁定中的内存模型问题,也使代码更简洁。
总结
双重检查锁定是一种在多线程环境中用于懒初始化的优化方法,能够减少不必要的锁开销。虽然传统的双重检查锁定模式在某些情况下存在内存模型问题,但通过 C++11 提供的新特性如 std::atomic 和 std::call_once,我们可以更安全地实现这一模式,并确保线程安全性。