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

在多线程环境下,单例模式需要保证在并发访问时仍然只创建一次实例。C++11之后,标准库提供了原子操作和内存序列化机制,结合局部静态变量的特性,可以轻松实现线程安全的单例。下面给出两种常见实现方式,并说明其优缺点。

1. 局部静态变量(Meyer’s Singleton)

class Singleton {
public:
    static Singleton& instance() {
        static Singleton instance;   // 只会初始化一次
        return instance;
    }
    // 禁止复制和移动
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
private:
    Singleton() = default;
    ~Singleton() = default;
};
  • 原理:C++11保证局部静态变量在第一次访问时以线程安全的方式初始化。此时只会有一次初始化,后续并发访问不会重复创建对象。
  • 优点:实现简洁,几乎无运行时开销,编译器负责同步。
  • 缺点:无法在运行时决定单例的生命周期(例如需要在程序结束前显式销毁),且如果单例构造函数抛异常,后续访问会导致再次尝试初始化。

2. 双重检查锁(Double-Checked Locking)

class Singleton {
public:
    static Singleton* instance() {
        Singleton* tmp = instance_.load(std::memory_order_acquire);
        if (!tmp) {
            std::lock_guard<std::mutex> lock(mutex_);
            tmp = instance_.load(std::memory_order_relaxed);
            if (!tmp) {
                tmp = new Singleton();
                instance_.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }
    // ...
private:
    Singleton() = default;
    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
  • 原理:先快速检查原子指针是否为空,若不为空直接返回;若为空则进入互斥锁保护的临界区,再次检查后创建实例并更新原子指针。memory_order_acquire/release保证了内存可见性。
  • 优点:可以在需要时显式释放单例(通过删除实例并置空原子指针),在高并发场景下读操作无锁,写操作仅在首次初始化时有锁。
  • 缺点:实现复杂度高,错误使用容易导致数据竞争或ABA问题;需要手动管理内存。

3. std::call_oncestd::once_flag

class Singleton {
public:
    static Singleton& instance() {
        std::call_once(flag_, [](){ instance_ = new Singleton(); });
        return *instance_;
    }
private:
    Singleton() = default;
    static Singleton* instance_;
    static std::once_flag flag_;
};

Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
  • 原理std::call_once保证给定的lambda只会执行一次,即使有多个线程并发调用。内部使用std::once_flag管理同步状态。
  • 优点:简洁安全,适合需要一次性初始化且不想使用局部静态变量的场景。
  • 缺点:与双重检查锁类似,需要手动销毁实例。

4. 线程安全的懒加载单例

有时单例实例化成本较高,可能希望延迟到第一次真正使用时才创建。上述局部静态变量已实现懒加载,但如果需要更细粒度的控制(例如在单例内部管理多种资源),可以使用 std::shared_ptrstd::weak_ptr

class Singleton {
public:
    static std::shared_ptr <Singleton> getInstance() {
        std::lock_guard<std::mutex> lock(mutex_);
        if (auto sp = instance_.lock()) {
            return sp;
        }
        auto sp = std::shared_ptr <Singleton>(new Singleton());
        instance_ = sp;
        return sp;
    }
private:
    Singleton() = default;
    static std::weak_ptr <Singleton> instance_;
    static std::mutex mutex_;
};

std::weak_ptr <Singleton> Singleton::instance_;
std::mutex Singleton::mutex_;
  • 特点:允许外部 shared_ptr 共享实例,实例在最后一个引用失效时自动销毁,避免手动删除。线程安全由互斥锁保证。

5. 总结

  • 推荐使用:如果单例不需要在运行时销毁,最简单且最安全的方案是使用局部静态变量(Meyer’s Singleton)。
  • 需要显式销毁或特殊初始化:可考虑 std::call_once 或双重检查锁。
  • 懒加载并可销毁:使用 std::weak_ptr+shared_ptr+互斥锁。

通过合理选择实现方式,可以在 C++ 中获得高效、线程安全且易维护的单例模式。

发表评论