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

在 C++11 之后,语言标准引入了对多线程原语的直接支持,例如 std::mutexstd::lock_guard、以及原子操作等。借助这些工具,我们可以很方便地实现一个线程安全的单例(Singleton)模式。以下内容将从理论到实践,逐步展示如何构建并使用一个线程安全的单例。

1. 单例模式回顾

单例模式保证一个类只有一个实例,并提供全局访问点。传统实现方式通常采用懒加载(lazy initialization)与双重检查锁(double-checked locking)或使用 static 局部变量(Meyer’s singleton)。

在单线程环境下,这些实现都足够,但当多线程同时访问单例初始化时,可能会出现竞争条件,导致实例被多次创建或访问到未完成构造的对象。

2. C++11 的 static 局部变量

C++11 规范保证了局部静态变量在多线程环境下的初始化是线程安全的。最简洁的单例实现如下:

class ThreadSafeSingleton {
public:
    static ThreadSafeSingleton& getInstance() {
        static ThreadSafeSingleton instance; // C++11 保证线程安全初始化
        return instance;
    }

    // 其它业务方法
    void doSomething() { /* ... */ }

private:
    ThreadSafeSingleton() = default;
    ~ThreadSafeSingleton() = default;
    ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
    ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
};

优点

  • 代码简洁,易于维护。
  • 运行时开销几乎为零,只有一次锁的内部检查。

缺点

  • 如果实例需要提前销毁(如在 atexit 前),需要显式手动销毁。
  • 在极端情况下,初始化时出现异常会导致以后无法再次获取实例。

3. 经典双重检查锁实现(C++11 兼容)

如果你想更显式地掌控锁的行为,可以使用 std::call_once 或手动实现双重检查锁。下面给出 std::call_once 的实现:

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

    void doSomething() { /* ... */ }

private:
    ThreadSafeSingleton() = default;
    ~ThreadSafeSingleton() = default;
    ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
    ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;

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

// 静态成员定义
std::unique_ptr <ThreadSafeSingleton> ThreadSafeSingleton::instance;
std::once_flag ThreadSafeSingleton::initFlag;

说明

  • std::call_once 确保 lambda 只执行一次,且线程安全。
  • std::once_flag 为一次性标志。
  • std::unique_ptr 用于管理实例的生命周期,避免手动 delete

4. 原子指针 + Compare-And-Swap(CAS)实现

如果你希望完全手动控制,或者在不想使用 std::call_once 的旧编译器下实现,可以使用原子指针和 CAS 操作:

#include <atomic>

class ThreadSafeSingleton {
public:
    static ThreadSafeSingleton& getInstance() {
        ThreadSafeSingleton* tmp = instance.load(std::memory_order_acquire);
        if (!tmp) {
            ThreadSafeSingleton* newInstance = new ThreadSafeSingleton;
            if (!instance.compare_exchange_strong(tmp, newInstance,
                                                  std::memory_order_release,
                                                  std::memory_order_acquire)) {
                delete newInstance; // 已有人创建,回收新建实例
            } else {
                tmp = newInstance;
            }
        }
        return *tmp;
    }

    void doSomething() { /* ... */ }

private:
    ThreadSafeSingleton() = default;
    ~ThreadSafeSingleton() = default;
    ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
    ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;

    static std::atomic<ThreadSafeSingleton*> instance;
};

// 静态成员定义
std::atomic<ThreadSafeSingleton*> ThreadSafeSingleton::instance{nullptr};

优点

  • 没有锁的开销。
  • 适合极高频的单例访问场景。

缺点

  • 代码复杂,易出现错误。
  • 需要手动管理内存,易产生泄漏。

5. 如何选择?

方法 代码复杂度 运行时开销 兼容性 典型使用场景
Meyer’s singleton 简单 C++11+ 通用场景
std::call_once 中等 C++11+ 需要显式控制
原子 + CAS C++11+ 高并发、性能敏感

在大多数项目中,Meyer’s singleton 就足够使用;如果你需要更细粒度的控制或者想让编译器知道你在考虑多线程,使用 std::call_once 是更安全的选择。

6. 线程安全的单例常见陷阱

  1. 对象的懒初始化与销毁时机冲突

    • 线程正在访问实例时,主线程可能在 atexit 期间销毁它,导致悬空指针。
    • 解决方案:让单例在整个程序生命周期内存在,或使用 std::shared_ptrstd::weak_ptr 的组合。
  2. 拷贝构造/赋值被遗漏

    • 必须显式 delete 拷贝构造函数和赋值运算符,否则会出现多实例。
  3. 异常安全

    • 如果构造函数抛异常,保证不留下半构造对象。
    • std::call_once 与原子实现天然异常安全;Meyer’s singleton 需要在 try-catch 中包装。
  4. 多进程环境

    • 单例只在进程内保证唯一性;在多进程共享内存等场景下,需要额外同步。

7. 结语

C++11 之后,单例模式的线程安全实现变得异常简单。开发者可以根据项目需求、性能要求以及代码维护的便利性,在三种主流实现方式中进行选择。只要遵循基本原则(禁止拷贝、保证一次性初始化、异常安全),就能得到一个既可靠又高效的全局对象。

发表评论