单例模式仅初始化时加锁是否足够?线程安全存疑
关于单例模式双重检查锁的线程安全误区
你提到的问题完全正确,双重检查锁(Double-Checked Locking Pattern, DCLP)仅负责保证单例实例初始化过程的线程安全,它无法解决实例内部共享状态被多线程并发修改时的竞态问题。
问题根源
当多个线程成功获取到同一个单例实例后,对实例内的共享变量执行「读取旧值→计算→写入新值」这类非原子操作时,若没有额外的同步机制,就会出现你描述的情况:
- 线程1读取共享变量后被中断
- 线程2用同一旧值完成修改并写入
- 线程1恢复后用旧值计算,直接覆盖线程2的修改,导致数据不一致
解决方法
要解决这个问题,需要针对单例内部的共享状态操作单独添加同步机制,常见的两种方案如下:
方案1:给共享操作加互斥锁
通过实例内部的互斥锁,保证每次只有一个线程能修改共享变量:
#include <mutex> class Singleton { private: static Singleton* instance; static std::mutex init_mutex_; // 仅用于初始化的锁 std::mutex state_mutex_; // 保护内部共享状态的锁 int shared_val_ = 1; // 示例共享变量 public: static Singleton* getInstance() { if (instance == nullptr) { std::lock_guard<std::mutex> lock(init_mutex_); if (instance == nullptr) { instance = new Singleton; } } return instance; } void Multiply2() { std::lock_guard<std::mutex> lock(state_mutex_); shared_val_ *= 2; } void Multiply3() { std::lock_guard<std::mutex> lock(state_mutex_); shared_val_ *= 3; } }; // 静态成员初始化 Singleton* Singleton::instance = nullptr; std::mutex Singleton::init_mutex_;
方案2:使用原子变量(适合简单数值操作)
利用C++的std::atomic类型,通过CAS(Compare-And-Swap)操作实现无锁的原子修改:
#include <mutex> #include <atomic> class Singleton { private: static Singleton* instance; static std::mutex init_mutex_; std::atomic<int> shared_val_ = 1; public: static Singleton* getInstance() { if (instance == nullptr) { std::lock_guard<std::mutex> lock(init_mutex_); if (instance == nullptr) { instance = new Singleton; } } return instance; } void Multiply2() { int old_val = shared_val_.load(std::memory_order_relaxed); // CAS循环直到操作成功 while (!shared_val_.compare_exchange_weak(old_val, old_val * 2)) {} } void Multiply3() { int old_val = shared_val_.load(std::memory_order_relaxed); while (!shared_val_.compare_exchange_weak(old_val, old_val * 3)) {} } }; // 静态成员初始化 Singleton* Singleton::instance = nullptr; std::mutex Singleton::init_mutex_;
关键提醒
很多开发者容易混淆两个不同的线程安全场景:
- 单例实例的初始化安全:由DCLP解决
- 单例实例内部状态的并发修改安全:需要额外的同步措施(锁/原子变量)
这是两个独立的问题,DCLP不会自动覆盖后者的线程安全需求。
内容的提问来源于stack exchange,提问作者Ali K.
相关产品推荐
相关产品推荐

