单例模式(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_once与std::once_flag确保只执行一次初始化,且线程安全。 - 可用于需要自定义析构顺序的情况(例如在
atexit里手动删除实例)。
缺点
- 代码稍长,维护成本略高。
- 仍然采用单例指针方式,可能导致悬挂指针或多次 delete 的风险。
3. std::shared_ptr 与 std::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_flag或std::shared_ptr与std::weak_ptr的组合。 - 双检查锁可以在高并发读多写少的场景中提升性能,但实现要格外谨慎。
在实际项目中,往往优先考虑简洁和可维护性。除非有明确的性能瓶颈或生命周期需求,否则建议使用局部静态变量实现。这样既能保证线程安全,又能避免额外的锁开销。