在 C++ 中如何实现线程安全的单例模式?

在多线程环境下,一个类的单例实现必须保证:

  1. 只产生一个实例;
  2. 在所有线程间共享同一实例;
  3. 对象初始化与销毁过程线程安全。

下面给出几种常见的实现方式,并说明各自的优缺点。


1. C++11 本地静态变量(Meyers 单例)

class ThreadSafeSingleton {
public:
    static ThreadSafeSingleton& instance() {
        static ThreadSafeSingleton inst;   // C++11 规范保证线程安全
        return inst;
    }
    // 删除拷贝构造和赋值操作
    ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
    ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;

private:
    ThreadSafeSingleton() { /* 初始化 */ }
    ~ThreadSafeSingleton() { /* 清理 */ }
};

优点

  • 简单、无锁实现;
  • 编译器自动保证线程安全;
  • 对象销毁按顺序执行,避免悬挂指针。

缺点

  • 只能在首次调用时延迟初始化;
  • 若对象初始化需要耗时,可能导致线程阻塞。
  • 对析构函数执行顺序有一定限制(若存在相互依赖的单例,可能导致析构错误)。

2. 带双重检查锁(DCL) + std::atomic

#include <atomic>
#include <mutex>

class DoubleCheckSingleton {
public:
    static DoubleCheckSingleton* instance() {
        DoubleCheckSingleton* tmp = ptr.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lk(mtx);
            tmp = ptr.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new DoubleCheckSingleton();
                ptr.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }

    ~DoubleCheckSingleton() { /* 清理 */ }

private:
    DoubleCheckSingleton() { /* 初始化 */ }
    DoubleCheckSingleton(const DoubleCheckSingleton&) = delete;
    DoubleCheckSingleton& operator=(const DoubleCheckSingleton&) = delete;

    static std::atomic<DoubleCheckSingleton*> ptr;
    static std::mutex mtx;
};

std::atomic<DoubleCheckSingleton*> DoubleCheckSingleton::ptr{nullptr};
std::mutex DoubleCheckSingleton::mtx;

优点

  • 对首次实例化进行一次加锁,随后只做原子加载,开销低;
  • 对旧的 C++ 标准(C++03)兼容。

缺点

  • 代码复杂,容易出错;
  • 仍然需要手动管理内存(new/delete),可能出现内存泄漏。
  • 在某些编译器/CPU 上可能出现“可见性”问题,导致实例不完全初始化。

3. std::call_once + std::once_flag

#include <mutex>

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

private:
    OnceFlagSingleton() { /* 初始化 */ }
    ~OnceFlagSingleton() { /* 清理 */ }
    OnceFlagSingleton(const OnceFlagSingleton&) = delete;
    OnceFlagSingleton& operator=(const OnceFlagSingleton&) = delete;

    static std::unique_ptr <OnceFlagSingleton> ptr;
    static std::once_flag initFlag;
};

std::unique_ptr <OnceFlagSingleton> OnceFlagSingleton::ptr = nullptr;
std::once_flag OnceFlagSingleton::initFlag;

优点

  • 语义清晰、代码简洁;
  • 标准库保证跨平台线程安全;
  • 通过 unique_ptr 自动销毁,避免内存泄漏。

缺点

  • std::call_once 的实现内部仍有锁,首次初始化会阻塞;
  • 对对象构造时间过长时,可能导致主线程等待。

4. 线程本地存储(TLS)实现的单例(适用于需要每个线程各自拥有一个实例的场景)

class ThreadLocalSingleton {
public:
    static ThreadLocalSingleton& instance() {
        thread_local ThreadLocalSingleton inst;
        return inst;
    }
    // ...
};

优点

  • 线程间完全隔离,避免共享冲突;
  • 每个线程都能即时访问自身实例。

缺点

  • 如果业务需求确实需要全局唯一实例,则不合适。
  • 需要自行管理每个线程的销毁顺序。

5. 关键点总结

方法 线程安全性 内存占用 锁开销 适用场景
C++11 静态局部 仅一次 0 适合轻量实例
双重检查锁 需要手动释放 兼容旧标准
std::call_once 通过 unique_ptr 自动 兼容旧标准、易用
TLS 线程本地 0 需要线程隔离
C++20 std::sync(不常见) 取决 新标准实验性

6. 小结

在 C++ 中实现线程安全的单例最推荐的方式是使用 C++11 及以后的 static 局部变量(Meyers 单例)或 std::call_once,它们既简单又可靠。若项目必须支持 C++03 或更早的标准,则可以考虑双重检查锁或 std::once_flag 的旧实现。根据业务需求(是否需要每线程单例、对象构造开销等)选择最合适的方法即可。

发表评论