在多线程环境下,单例模式的实现需要特别小心,避免出现竞态条件导致实例被多次创建。下面将介绍几种常见的线程安全单例实现方式,并对它们的优缺点进行分析。
1. 懒汉式(双重检查锁)
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
if (instance_ == nullptr) {
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr) {
instance_ = new Singleton();
}
}
return *instance_;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
优点
- 延迟初始化,第一次使用时才创建实例。
缺点
- 需要额外的锁和判断,性能稍低。
- 在C++11之前,
double-checked locking并不安全,需使用std::atomic或其他同步手段。
2. 饿汉式(编译期初始化)
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;
};
优点
- 简单,编译器保证线程安全。
- 不需要显式的锁,性能更好。
缺点
- 早期创建,若对象消耗资源且不一定使用,可能浪费。
3. 局部静态变量+Meyers单例(推荐)
该方式与饿汉式类似,但通过函数内部的局部静态对象实现懒加载。
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11+ 线程安全
return instance;
}
// ...
};
优势
- 兼具懒加载与线程安全。
- 代码简洁易懂。
4. 采用 std::call_once 与 std::once_flag
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(flag_, [](){ instance_ = new Singleton(); });
return *instance_;
}
// ...
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
优势
- 只需一次初始化,适合需要自定义构造过程的场景。
- 可与动态加载库配合使用,避免构造顺序问题。
5. 对于全局析构顺序的处理
在多线程程序中,程序结束时所有单例对象的销毁顺序可能导致“静态释放顺序问题”。常见解决办法:
- 使用
std::shared_ptr:让单例持有一个std::shared_ptr,当所有引用释放后自动销毁。 - 使用
atexit:显式注册析构函数,保证按期望顺序调用。 - 懒销毁:不显式销毁单例,利用程序结束时操作系统回收资源。
6. 何时选择哪种实现
| 场景 | 推荐实现 |
|---|---|
| 必须在程序最早阶段就可用,且不占用过多资源 | 饿汉式 |
| 想要延迟创建、资源占用较大 | Meyers 单例或 std::call_once |
| 需要跨平台、跨编译器保证兼容 | std::call_once + std::once_flag |
| 关注析构顺序问题 | std::shared_ptr 或 atexit |
7. 常见坑与注意事项
- 复制构造和赋值:一定要禁用,防止出现多个实例。
- 线程局部存储:若单例持有线程局部变量,需要考虑析构时机。
- 构造顺序:在多线程程序中,如果单例在某些线程中被提前创建,其他线程可能无法正确获取。
- 异常安全:构造期间抛异常时,确保实例不会留在堆中。
8. 结语
在C++11之后,利用局部静态变量实现的Meyers单例几乎是最推荐的做法。它兼顾懒加载、线程安全,并且代码最简洁。若项目需要更细粒度的控制,std::call_once 仍是强有力的工具。无论采用哪种实现方式,禁用复制构造与赋值、处理好析构顺序都是保证单例健壮性的关键。