### 如何在 C++ 中实现一个线程安全的懒汉式单例模式?

在多线程环境下,单例模式需要保证对象只被创建一次,并且所有线程都能安全访问该实例。下面介绍几种常用实现方式,并讨论它们的优缺点。

1. 基于 std::call_once 的实现(C++11+)

#include <iostream>
#include <mutex>

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;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

    void doSomething() { std::cout << "Hello from Singleton\n"; }

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

    static std::unique_ptr <Singleton> instance;
    static std::once_flag initFlag;
};

std::unique_ptr <Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;

优点

  • 线程安全,std::call_once 确保初始化块只执行一次。
  • 延迟初始化,只有首次调用 getInstance() 时才创建对象。
  • 代码简洁、易维护。

缺点

  • std::once_flag 的实现依赖底层平台,可能在极少数环境下表现不稳定,但在标准 C++ 环境下已足够稳健。

2. 基于局部静态变量的实现(C++11+)

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;  // C++11 起线程安全
        return instance;
    }
    // 其它成员同上
};

优点

  • 代码最简洁,直接利用编译器对局部静态的线程安全保证。
  • 延迟初始化与 std::call_once 兼容。

缺点

  • 对于需要在程序结束前显式销毁对象的情况,无法控制销毁顺序,可能导致“静态销毁顺序问题”。
  • 在某些编译器中,如果使用了 -fno-threadsafe-statics 选项,可能失去线程安全。

3. 双重检查锁(Double-Check Locking)

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

优点

  • 在第一次实例化后,后续调用不再涉及锁,性能更好。

缺点

  • 需要对 instance 做原子操作,若未使用 std::atomic,在某些体系结构上会出现可见性问题。
  • 代码复杂度较高,容易出现细节错误,现代 C++ 推荐使用 std::call_once 或局部静态变量。

4. Meyers 单例(C++03 兼容)

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;  // C++03 对静态局部变量的初始化不是线程安全的
        return instance;
    }
};

优点

  • 兼容 C++03,适用于旧编译器。

缺点

  • 不是线程安全,需要额外同步机制。
  • 与 C++11 版本相比,需手动加锁。

5. 需要注意的细节

细节 说明
析构顺序 对于单例使用 std::unique_ptr 或局部静态变量时,析构顺序由实现决定,可能导致在其他全局对象析构时使用已被销毁的单例。若不想出现此问题,可采用“懒初始化 + 销毁标志”或将单例设计为“永不过期”。
异常安全 std::call_once 的初始化 lambda 必须保证不抛出异常,否则后续调用会再次触发初始化。
多线程读取 单例提供的接口应尽量避免共享可变状态,或者内部使用读写锁、原子变量等保证线程安全。
性能评估 对于高频读访问的单例,建议使用局部静态变量或 std::call_once,避免每次都获取互斥锁。

6. 结论

在现代 C++(C++11 及以后)中,最推荐的实现方式是:

class Singleton {
public:
    static Singleton& getInstance() {
        static Singleton instance;   // C++11 起线程安全
        return instance;
    }
    // ...
};

或者使用 std::call_once 的显式实现,两者都具备延迟初始化、线程安全、易维护等优点。只有在需要更细粒度控制生命周期或销毁顺序时,才考虑使用更复杂的方案。

发表评论