在多线程环境下,单例模式(Singleton)需要确保只有一个实例被创建,并且在任何线程访问时都能获得同一实例。下面以 C++17 为例,演示几种常见的实现方式,并说明它们的优缺点。
-
C++11 的局部静态变量(Meyers’ Singleton)
class Singleton { public: static Singleton& instance() { static Singleton instance; // 第一次调用时初始化 return instance; } Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; private: Singleton() = default; };- 优点:编译器保证线程安全的局部静态初始化(C++11 起)。代码简洁,易于维护。
- 缺点:在实例销毁时,如果其他线程仍在使用实例,可能导致未定义行为;且无法控制实例销毁时机(通常在程序退出时自动销毁)。
-
带双重检查锁(Double-Check Locking)
#include <atomic> #include <mutex> 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_;- 优点:延迟实例化且在首次调用前不占用锁。
- 缺点:实现复杂,错误易产生。需要使用
std::atomic和适当的内存顺序,否则仍可能出现竞态。
-
使用
std::call_once与std::once_flag#include <mutex> class Singleton { public: static Singleton& instance() { std::call_once(flag_, [](){ instance_.reset(new Singleton); }); return *instance_; } // 其余与第一种相同 private: Singleton() = default; static std::unique_ptr <Singleton> instance_; static std::once_flag flag_; }; std::unique_ptr <Singleton> Singleton::instance_; std::once_flag Singleton::flag_;- 优点:线程安全的单一初始化,代码可读性好,适合需要在运行时决定是否实例化的场景。
- 缺点:与第一个实现类似,销毁时机不易控制。
-
惰性销毁(Lazy Destruction)
如果你想在程序结束时不必担心销毁顺序,可以使用std::shared_ptr配合std::weak_ptr。class Singleton { public: static std::shared_ptr <Singleton> instance() { static std::weak_ptr <Singleton> weak_instance; std::shared_ptr <Singleton> strong_instance = weak_instance.lock(); if (!strong_instance) { strong_instance = std::make_shared <Singleton>(); weak_instance = strong_instance; } return strong_instance; } private: Singleton() = default; };- 优点:实例在最后一个
shared_ptr被销毁时释放,避免了全局析构顺序问题。 - 缺点:需要每次访问返回
std::shared_ptr,如果频繁调用会产生轻微开销。
- 优点:实例在最后一个
-
C++20 的
std::atomic<std::shared_ptr<T>>
对于需要共享单例对象且线程安全的读写,C++20 提供了原子化shared_ptr。class Singleton { public: static std::shared_ptr <Singleton> instance() { std::shared_ptr <Singleton> ptr = instance_.load(std::memory_order_acquire); if (!ptr) { std::shared_ptr <Singleton> new_ptr = std::make_shared<Singleton>(); if (instance_.compare_exchange_strong(ptr, new_ptr, std::memory_order_release, std::memory_order_relaxed)) { ptr = new_ptr; } } return ptr; } private: Singleton() = default; static std::atomic<std::shared_ptr<Singleton>> instance_; }; std::atomic<std::shared_ptr<Singleton>> Singleton::instance_;- 优点:原子操作保证多线程安全,且无需显式锁。
- 缺点:依赖 C++20,可能与旧编译器不兼容。
小结
- 最推荐:C++11 局部静态变量(Meyers’ Singleton)——实现简单,编译器保证线程安全。
- 若需更细粒度控制:
std::call_once或std::once_flag。 - 若担心销毁顺序:使用
std::shared_ptr或std::weak_ptr。 - 多线程读写共享实例:C++20 原子化
shared_ptr。
在实际项目中,往往把单例设计成 “延迟初始化 + 线程安全” 的形式,并在构造函数中完成必要的资源准备。需要注意的是,单例模式并非万能,若滥用会导致全局状态污染、单元测试困难以及并发死锁风险。使用时请结合项目实际需求和团队经验进行选择。