在现代 C++(尤其是 C++11 及之后的标准)中,线程安全的单例模式不再需要复杂的锁机制。标准库已经提供了几种天然线程安全的实现方式,下面详细介绍几种常见的实现方案,并对比它们的优缺点。
1. 函数内部静态局部对象(Meyer’s Singleton)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 保证线程安全
return instance;
}
// 禁止拷贝和移动构造
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
关键点
- 局部静态变量:在第一次调用
getInstance()时初始化,随后返回同一实例。 - C++11 线程安全:标准保证局部静态变量的初始化是线程安全的。只要构造不抛异常,后续访问不会出现竞争。
- 懒加载:实例化时机完全由
getInstance()的调用决定,符合大多数单例需求。
适用场景
- 对象生命周期不需要提前控制(默认在程序结束时析构)。
- 只需要一次初始化,且构造不复杂。
可能的问题
- 构造抛异常:若构造函数抛异常,下一次调用会重新尝试初始化,导致重复构造。若此行为不可接受,可以采用显式初始化策略。
- 析构顺序:如果单例在其他全局对象之前析构,可能导致析构时访问已被销毁的资源。通常不需要担心,但在复杂项目中需谨慎。
2. std::call_once 与 std::once_flag
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, [](){
instance.reset(new Singleton);
});
return *instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static std::unique_ptr <Singleton> instance;
static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;
关键点
std::call_once:确保闭包只执行一次,线程安全。- 显式控制实例生命周期:通过
unique_ptr可以在需要时手动销毁实例,例如在atexit或者自定义函数中调用instance.reset()。
适用场景
- 需要在单例创建前执行额外逻辑(如配置读取)。
- 想在程序结束前显式销毁单例,防止全局析构顺序问题。
3. 线程安全的懒汉式双检锁(双重检查锁定)
在 C++11 之前,双重检查锁定经常用于实现单例,但在旧标准下存在内存可见性问题。C++11 之后可以安全实现,但通常不建议使用,因为前两种方式更简洁、可读。
class Singleton {
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx);
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 mtx;
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;
适用场景
- 需要极端优化实例化时的竞争开销,且对代码复杂度容忍度高。
4. 现代 C++ 中的 std::shared_ptr 与 std::make_shared
如果单例需要共享所有权,可以使用 shared_ptr:
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
std::call_once(initFlag, [](){
instance = std::make_shared <Singleton>();
});
return instance;
}
private:
Singleton() = default;
static std::shared_ptr <Singleton> instance;
static std::once_flag initFlag;
};
关键点
shared_ptr自动管理引用计数,适用于需要多处持有实例的场景。- 需要注意循环引用和析构时机。
5. 需要考虑的细节
| 细节 | 说明 |
|---|---|
| 构造异常 | call_once 或局部静态变量会在构造异常后允许再次尝试;如果不想重复构造,最好在构造前完成所有初始化逻辑或使用 try/catch 包装。 |
| 析构顺序 | 对于全局单例,C++ 的析构顺序不确定,尤其是在多模块项目中。使用 call_once 并在 atexit 手动销毁可规避问题。 |
| 多线程读取 | 在 C++11 之后,std::atomic 或 std::call_once 提供了充分的同步保证,普通读取操作无需额外锁。 |
| 延迟初始化 | std::call_once 与局部静态变量都属于“懒加载”,直到第一次使用时才实例化。若想在程序启动即初始化,可使用构造函数或显式初始化调用。 |
6. 小结
- 推荐实现:函数内部静态局部对象(Meyer’s Singleton)——简单、线程安全、懒加载。
- 若需显式销毁:
std::call_once与std::unique_ptr结合。 - 复杂需求:双检锁或
shared_ptr方案,但代码复杂度较高。
通过上述几种实现,你可以根据项目需求选择最合适的单例模式。无论选择哪种,都应当避免在单例内部使用全局状态或产生副作用,以保持代码的可维护性与可测试性。