**实现一个线程安全的双重检查锁定单例模式**

在 C++ 中实现单例模式时,常常需要兼顾线程安全和性能。双重检查锁定(Double‑Checked Locking)是一种常用且高效的做法,它只在第一次实例化时进行加锁,之后的访问则无需加锁,从而避免了不必要的同步开销。下面将给出一个现代 C++(C++11 及以后)实现,并对其细节进行说明。


1. 基本思路

class Singleton {
public:
    static Singleton& instance();   // 访问唯一实例
    // 禁止拷贝构造和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    Singleton() = default;          // 私有构造函数
    ~Singleton() = default;
};
  • 私有构造:保证外部无法创建实例。
  • 禁止拷贝:防止通过拷贝构造/赋值产生多份实例。
  • instance():返回唯一实例的引用。

2. 双重检查锁定实现

#include <mutex>

Singleton& Singleton::instance() {
    static Singleton* instancePtr = nullptr; // 原始指针
    static std::mutex mtx;                  // 互斥锁

    if (!instancePtr) {                     // 第一次检查(无锁)
        std::lock_guard<std::mutex> lock(mtx); // 加锁
        if (!instancePtr) {                 // 第二次检查(有锁)
            instancePtr = new Singleton();
        }
    }
    return *instancePtr;
}

关键点说明

  1. 局部静态指针

    • static Singleton* instancePtr = nullptr;
      这保证了 instancePtr 的生命周期贯穿整个程序,且只会被初始化一次。
  2. 双重检查

    • 第一层 if (!instancePtr) 在没有锁的情况下检查是否已实例化,避免了每次访问都需要锁的开销。
    • 第二层 if (!instancePtr) 在锁定后再次检查,确保多线程情况下只创建一次实例。
  3. 互斥锁

    • std::mutex mtx; 只在第一次创建时使用。
    • 使用 std::lock_guard 确保异常安全。
  4. 原子性与内存序

    • 在 C++11 及以后,static 局部变量的初始化已被保证为线程安全,但由于我们使用了裸指针并手动控制初始化,需自行保证同步。
    • std::mutexlock()unlock() 提供了必要的内存屏障,确保指针写入对其他线程可见。

3. 现代 C++ 推荐方案

从 C++11 开始,最简洁且最安全的实现方式是使用 局部静态对象(Meyer’s Singleton):

Singleton& Singleton::instance() {
    static Singleton instance;   // 编译器保证线程安全
    return instance;
}
  • 优点:无需手动管理锁,代码更简洁。
  • 缺点:实例化时机不可控,可能在程序结束时被析构,导致对 instance() 的后续调用产生悬挂指针。

如果你需要手动控制生命周期或需要在程序结束前确保资源已被释放,可以继续使用双重检查锁定方式。

4. 完整代码示例

#include <iostream>
#include <mutex>
#include <thread>

class Singleton {
public:
    static Singleton& instance() {
        static Singleton* instancePtr = nullptr;
        static std::mutex mtx;
        if (!instancePtr) {
            std::lock_guard<std::mutex> lock(mtx);
            if (!instancePtr) {
                instancePtr = new Singleton();
            }
        }
        return *instancePtr;
    }

    void do_something() const {
        std::cout << "Singleton instance at " << this << " performing action.\n";
    }

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

private:
    Singleton() { std::cout << "Singleton constructed.\n"; }
    ~Singleton() { std::cout << "Singleton destroyed.\n"; }
};

void worker(int id) {
    Singleton::instance().do_something();
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    std::thread t3(worker, 3);
    t1.join(); t2.join(); t3.join();
    return 0;
}

运行结果(可能输出)

Singleton constructed.
Singleton instance at 0x55c2a3b0d3c0 performing action.
Singleton instance at 0x55c2a3b0d3c0 performing action.
Singleton instance at 0x55c2a3b0d3c0 performing action.
Singleton destroyed.

5. 常见坑与解决方案

场景 问题 解决办法
单例构造异常 构造函数抛异常导致 instancePtr 仍为 nullptr new Singleton() 前捕获异常并清理已分配资源
程序结束后使用 程序结束后多次调用 instance() 可能使用已被析构的实例 使用 std::call_oncestd::unique_ptr 并提供显式销毁函数
需要延迟销毁 程序结束前需要执行清理工作 Singleton 析构中完成,或提供 shutdown() 方法

总结

  • 双重检查锁定提供了线程安全且高效的单例实现,适用于对实例化时机有严格要求的场景。
  • 现代 C++ 推荐使用局部静态对象实现单例,除非你需要手动控制生命周期。
  • 无论哪种方式,注意线程安全、异常安全以及程序结束时的资源释放,才能确保单例实现的可靠性。

发表评论