在多线程环境下,一个类的单例实现必须保证:
- 只产生一个实例;
- 在所有线程间共享同一实例;
- 对象初始化与销毁过程线程安全。
下面给出几种常见的实现方式,并说明各自的优缺点。
1. C++11 本地静态变量(Meyers 单例)
class ThreadSafeSingleton {
public:
static ThreadSafeSingleton& instance() {
static ThreadSafeSingleton inst; // C++11 规范保证线程安全
return inst;
}
// 删除拷贝构造和赋值操作
ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
private:
ThreadSafeSingleton() { /* 初始化 */ }
~ThreadSafeSingleton() { /* 清理 */ }
};
优点
- 简单、无锁实现;
- 编译器自动保证线程安全;
- 对象销毁按顺序执行,避免悬挂指针。
缺点
- 只能在首次调用时延迟初始化;
- 若对象初始化需要耗时,可能导致线程阻塞。
- 对析构函数执行顺序有一定限制(若存在相互依赖的单例,可能导致析构错误)。
2. 带双重检查锁(DCL) + std::atomic
#include <atomic>
#include <mutex>
class DoubleCheckSingleton {
public:
static DoubleCheckSingleton* instance() {
DoubleCheckSingleton* tmp = ptr.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lk(mtx);
tmp = ptr.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new DoubleCheckSingleton();
ptr.store(tmp, std::memory_order_release);
}
}
return tmp;
}
~DoubleCheckSingleton() { /* 清理 */ }
private:
DoubleCheckSingleton() { /* 初始化 */ }
DoubleCheckSingleton(const DoubleCheckSingleton&) = delete;
DoubleCheckSingleton& operator=(const DoubleCheckSingleton&) = delete;
static std::atomic<DoubleCheckSingleton*> ptr;
static std::mutex mtx;
};
std::atomic<DoubleCheckSingleton*> DoubleCheckSingleton::ptr{nullptr};
std::mutex DoubleCheckSingleton::mtx;
优点
- 对首次实例化进行一次加锁,随后只做原子加载,开销低;
- 对旧的 C++ 标准(C++03)兼容。
缺点
- 代码复杂,容易出错;
- 仍然需要手动管理内存(new/delete),可能出现内存泄漏。
- 在某些编译器/CPU 上可能出现“可见性”问题,导致实例不完全初始化。
3. std::call_once + std::once_flag
#include <mutex>
class OnceFlagSingleton {
public:
static OnceFlagSingleton& instance() {
std::call_once(initFlag, [](){
ptr.reset(new OnceFlagSingleton());
});
return *ptr;
}
private:
OnceFlagSingleton() { /* 初始化 */ }
~OnceFlagSingleton() { /* 清理 */ }
OnceFlagSingleton(const OnceFlagSingleton&) = delete;
OnceFlagSingleton& operator=(const OnceFlagSingleton&) = delete;
static std::unique_ptr <OnceFlagSingleton> ptr;
static std::once_flag initFlag;
};
std::unique_ptr <OnceFlagSingleton> OnceFlagSingleton::ptr = nullptr;
std::once_flag OnceFlagSingleton::initFlag;
优点
- 语义清晰、代码简洁;
- 标准库保证跨平台线程安全;
- 通过
unique_ptr自动销毁,避免内存泄漏。
缺点
std::call_once的实现内部仍有锁,首次初始化会阻塞;- 对对象构造时间过长时,可能导致主线程等待。
4. 线程本地存储(TLS)实现的单例(适用于需要每个线程各自拥有一个实例的场景)
class ThreadLocalSingleton {
public:
static ThreadLocalSingleton& instance() {
thread_local ThreadLocalSingleton inst;
return inst;
}
// ...
};
优点
- 线程间完全隔离,避免共享冲突;
- 每个线程都能即时访问自身实例。
缺点
- 如果业务需求确实需要全局唯一实例,则不合适。
- 需要自行管理每个线程的销毁顺序。
5. 关键点总结
| 方法 | 线程安全性 | 内存占用 | 锁开销 | 适用场景 |
|---|---|---|---|---|
| C++11 静态局部 | ✔ | 仅一次 | 0 | 适合轻量实例 |
| 双重检查锁 | ✔ | 需要手动释放 | 低 | 兼容旧标准 |
| std::call_once | ✔ | 通过 unique_ptr 自动 |
低 | 兼容旧标准、易用 |
| TLS | ✔ | 线程本地 | 0 | 需要线程隔离 |
C++20 std::sync(不常见) |
✔ | 取决 | 低 | 新标准实验性 |
6. 小结
在 C++ 中实现线程安全的单例最推荐的方式是使用 C++11 及以后的 static 局部变量(Meyers 单例)或 std::call_once,它们既简单又可靠。若项目必须支持 C++03 或更早的标准,则可以考虑双重检查锁或 std::once_flag 的旧实现。根据业务需求(是否需要每线程单例、对象构造开销等)选择最合适的方法即可。