双重检查锁定(Double-Checked Locking)


"双重检查锁定"(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,我们可以更安全地实现这一模式,并确保线程安全性。


相关推荐
孟秋与你18 小时前
【spring】spring单例模式与锁对象作用域的分析
java·spring·单例模式
程序员奇奥1 天前
设计模式——简单工厂模型、工厂模式、抽象工厂模式、单例模式、代理模式、模板模式
单例模式·设计模式·抽象工厂模式
p-knowledge1 天前
单例模式(Singleton Pattern)
单例模式
oioihoii1 天前
单例模式详解
c++·单例模式·c#·大学必学
春风十里不如你95272 天前
【设计模式】【创建型模式(Creational Patterns)】之单例模式
java·单例模式·设计模式
沐凡星4 天前
单例模式(Singleton)
开发语言·算法·单例模式
南城花随雪。4 天前
Mybatis框架之单例模式 (Singleton Pattern)
单例模式·mybatis
水w4 天前
单例模式的几种实现方式
java·开发语言·jvm·单例模式·单例
Atlasgorov5 天前
JAVA_单例模式
java·开发语言·单例模式
捕鲸叉5 天前
C++单例模式与多例模式
开发语言·c++·单例模式