【设计模式】单例模式之双检锁(Double-Checked Locking)

双检锁(Double-Checked Locking)是一种在多线程环境下高效实现单例模式的技术,它结合了延迟初始化线程安全的优点,避免了不必要的同步开销。

核心思想

双检锁的核心思想是:

  1. 第一重检查​(无锁):快速检查实例是否已创建
  2. 加锁保护​:确保只有一个线程进入创建流程
  3. 第二重检查​(有锁):再次检查实例是否已创建
  4. 创建实例​:如果仍未创建,则创建实例

经典实现(C++11之前)

#include 

class Singleton {
public:
    static Singleton* getInstance() {
        // 第一重检查:避免不必要的锁竞争
        if (instance == nullptr) {
            // 加锁保护创建过程
            std::lock_guard lock(mutex);
            
            // 第二重检查:防止多个线程同时通过第一重检查
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return instance;
    }

    // 禁用拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;

    static Singleton* instance;
    static std::mutex mutex;
};

// 初始化静态成员
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;

问题:指令重排导致的不安全性

在C++11之前,上述实现存在潜在问题:

instance = new Singleton();

这行代码实际上包含三个步骤:

  1. 分配内存
  2. 构造对象
  3. 将地址赋值给instance

编译器可能进行指令重排,导致执行顺序变为:

  1. 分配内存
  2. 将地址赋值给instance
  3. 构造对象

如果另一个线程在第一重检查时发现instance不为nullptr,就会返回一个尚未完全构造的对象!

C++11安全实现方案

方案1:使用原子操作和内存序

#include 
#include 

class Singleton {
public:
    static Singleton* getInstance() {
        // 使用memory_order_acquire保证读取顺序
        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();
                // 使用memory_order_release保证写入顺序
                instance.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;

    static std::atomic instance;
    static std::mutex mutex;
};

std::atomic Singleton::instance(nullptr);
std::mutex Singleton::mutex;

方案2:使用call_once(推荐)

#include 

class Singleton {
public:
    static Singleton& getInstance() {
        std::call_once(initFlag, []() {
            instance.reset(new Singleton());
        });
        return *instance;
    }

    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;
    ~Singleton() = default;

    static std::unique_ptr instance;
    static std::once_flag initFlag;
};

std::unique_ptr Singleton::instance;
std::once_flag Singleton::initFlag;

双检锁的优缺点

优点:

  1. 高效性​:只有第一次创建时需要加锁
  2. 延迟初始化​:资源按需创建
  3. 线程安全​:确保多线程环境下只创建一个实例

缺点:

  1. 实现复杂​:需要考虑指令重排和内存序
  2. 平台依赖​:不同编译器/平台可能有不同行为
  3. 析构问题​:需要额外处理对象销毁

双检锁 vs 其他单例实现

特性 双检锁 饿汉式 静态局部变量
线程安全 ✅ 是 ✅ 是 ✅ 是
延迟初始化 ✅ 是 ❌ 否 ✅ 是
实现复杂度 ⚠️ 中等 ⚠️ 中等 ✅ 简单
性能开销 ⚠️ 首次访问有锁 ✅ 无锁 ⚠️ 首次访问有隐式锁
内存管理 ⚠️ 需手动处理 ✅ 自动 ✅ 自动
C++版本要求 ⚠️ C++11+安全 所有 C++11+

双检锁的适用场景

  1. 初始化成本高​:创建实例需要大量资源
  2. 访问频率高​:实例会被频繁访问
  3. 多线程环境​:需要确保线程安全
  4. 旧代码维护​:兼容不支持C++11的代码库

最佳实践建议

  1. 优先使用现代C++特性​:

    // C++11+推荐写法
    static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
  2. 如果必须使用双检锁​:

    • 使用std::atomic和正确内存序
    • 或者使用std::call_once
    • 避免原始指针,使用智能指针
  3. 处理析构问题​:

    class Singleton {
    public:
        static Singleton& getInstance() {
            static Singleton instance;
            return instance;
        }
        
        ~Singleton() {
            // 清理资源
        }
        
    private:
        Singleton() = default;
    };
  4. 避免单例滥用​:

    • 考虑依赖注入
    • 评估是否真的需要全局唯一实例
    • 限制单例的使用范围

总结

双检锁模式是多线程环境下实现单例的有效方法,但现代C++(C++11及以上)提供了更简洁安全的替代方案。在必须使用双检锁时,务必注意指令重排问题,使用原子操作或std::call_once确保线程安全。对于新项目,优先考虑使用静态局部变量实现单例模式。

你可能感兴趣的:(#,软件模式,设计模式,单例模式)