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

单例模式(Singleton Pattern)是一种常用的软件设计模式,旨在保证一个类只有一个实例,并提供一个全局访问点。实现单例模式时,需要考虑多线程环境下的线程安全性。下面以 C++17 标准为例,介绍几种常用且线程安全的实现方式。

1. 局部静态变量(Meyers Singleton)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // C++11 起线程安全的局部静态初始化
        return instance;
    }
    // 禁止拷贝构造和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

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

优点

  • 简单直观,代码量少。
  • 由于 C++11 之后局部静态对象的初始化是线程安全的,能够避免多线程竞争。
  • 延迟初始化(第一次调用时才创建实例),节省资源。

缺点

  • 只能在函数内部创建,无法控制实例的销毁时机(在程序结束时由系统回收)。
  • 对于需要显式销毁或多次重置的场景不太适用。

2. 带锁的单例(显式互斥锁)

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

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

    static ThreadSafeSingleton* instance;
    static std::once_flag initFlag;
};

ThreadSafeSingleton* ThreadSafeSingleton::instance = nullptr;
std::once_flag ThreadSafeSingleton::initFlag;

优点

  • 通过 std::call_oncestd::once_flag 确保只执行一次初始化,且线程安全。
  • 可用于需要自定义析构顺序的情况(例如在 atexit 里手动删除实例)。

缺点

  • 代码稍长,维护成本略高。
  • 仍然采用单例指针方式,可能导致悬挂指针或多次 delete 的风险。

3. std::shared_ptrstd::weak_ptr 结合

class LazySingleton {
public:
    static std::shared_ptr <LazySingleton> getInstance() {
        std::lock_guard<std::mutex> lock(mtx);
        if (auto sp = instance.lock()) {
            return sp;
        }
        auto sptr = std::shared_ptr <LazySingleton>(new LazySingleton());
        instance = sptr;
        return sptr;
    }

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

    static std::weak_ptr <LazySingleton> instance;
    static std::mutex mtx;
};

std::weak_ptr <LazySingleton> LazySingleton::instance;
std::mutex LazySingleton::mtx;

优点

  • 支持 std::shared_ptr 自动管理生命周期,避免手动删除。
  • weak_ptr 防止循环引用,实例在所有共享指针被销毁后自动释放。
  • 可以在多线程环境下安全创建和销毁实例。

缺点

  • 需要手动加锁,性能略低于局部静态变量实现。
  • 代码更复杂,理解成本更高。

4. 使用 std::atomic 的双检查锁

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

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

    static std::atomic<AtomicSingleton*> instance;
    static std::mutex mtx;
};

std::atomic<AtomicSingleton*> AtomicSingleton::instance{nullptr};
std::mutex AtomicSingleton::mtx;

优点

  • 只在首次创建实例时加锁,后续访问无需加锁,性能更好。
  • 适用于高频访问单例的场景。

缺点

  • 实现复杂,易出现指针悬挂或内存泄漏。
  • 需要严格遵循 std::memory_order 的规范,错误使用会导致难以调试的 bug。

5. 何时使用哪种实现?

需求 推荐实现
仅需在程序结束时创建并销毁,延迟初始化 局部静态变量(Meyers)
需要手动销毁、显式析构顺序 带锁 + std::once_flag
需要自动管理生命周期,支持多处共享 shared_ptr + weak_ptr
高性能,多次访问,避免每次加锁 双检查锁 + std::atomic

6. 小结

  • C++11 起,局部静态变量的初始化已实现线程安全,最简洁的单例实现是使用 Meyers 单例模式。
  • 对于更复杂的生命周期管理,建议使用 std::once_flagstd::shared_ptrstd::weak_ptr 的组合。
  • 双检查锁可以在高并发读多写少的场景中提升性能,但实现要格外谨慎。

在实际项目中,往往优先考虑简洁和可维护性。除非有明确的性能瓶颈或生命周期需求,否则建议使用局部静态变量实现。这样既能保证线程安全,又能避免额外的锁开销。

发表评论