在多线程环境下实现线程安全的单例模式是一项常见但细节丰富的任务。下面给出几种常见实现方式,并说明各自的优缺点、适用场景以及常见陷阱。
1. 经典 Meyers 单例(C++11 及以后)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 函数内静态变量,C++11 保障线程安全
return instance;
}
// 禁止拷贝和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
private:
Singleton() {} // 私有构造函数
};
优点
- 代码简洁,几乎无成本。
- C++11 标准保证了线程安全的局部静态对象初始化。
- 不需要显式的互斥锁,避免了锁竞争。
缺点
- 需要 C++11 以上编译器支持。
- 对于提前销毁或延迟销毁的需求,无法控制。
适用场景
- 需要全局唯一实例,且实例生命周期与程序生命周期一致。
- 环境已支持 C++11 或以上。
2. 双重检查锁(Double-Check Locking)
class Singleton {
public:
static Singleton* instance() {
if (ptr == nullptr) { // 第一层检查
std::lock_guard<std::mutex> lock(mtx);
if (ptr == nullptr) { // 第二层检查
ptr = new Singleton();
}
}
return ptr;
}
// 禁止拷贝和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
private:
Singleton() {}
static Singleton* ptr;
static std::mutex mtx;
};
Singleton* Singleton::ptr = nullptr;
std::mutex Singleton::mtx;
优点
- 仅在首次实例化时使用锁,后续访问几乎无锁。
缺点
- 需要确保
ptr的可见性,必须使用std::atomic或volatile。若没有正确处理,可能导致在多核上出现“读到未初始化的实例”问题。 - 代码相对复杂,容易写错。
适用场景
- 需要在 C++11 之前的编译器上实现线程安全单例。
- 对锁成本要求极低。
3. 静态局部变量 + std::call_once
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, [](){
ptr = new Singleton();
});
return *ptr;
}
// 禁止拷贝和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
private:
Singleton() {}
static Singleton* ptr;
static std::once_flag initFlag;
};
Singleton* Singleton::ptr = nullptr;
std::once_flag Singleton::initFlag;
优点
- 明确表示“一次性初始化”,对编译器和运行时的内存模型友好。
- 不需要手动加锁,避免锁竞争。
缺点
- 依赖
std::once_flag,同样需要 C++11 以上。
适用场景
- 需要在多线程启动时保证一次性初始化,但不想使用局部静态对象。
4. 使用 std::shared_ptr 或 std::unique_ptr 的懒加载
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
static std::shared_ptr <Singleton> ptr;
static std::once_flag flag;
std::call_once(flag, [](){
ptr = std::make_shared <Singleton>();
});
return ptr;
}
// 禁止拷贝和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
private:
Singleton() {}
};
优点
- 通过
shared_ptr方便管理生命周期,支持多重持有。 - 结合
std::call_once保证线程安全。
缺点
- 额外的智能指针开销(引用计数)。
适用场景
- 单例需要在多处共享并且可能在不同模块销毁时保持可用。
常见陷阱与最佳实践
| 陷阱 | 解决方案 |
|---|---|
未使用 std::atomic 或 volatile 保护双重检查锁 |
使用 std::atomic<Singleton*> ptr 或改用 std::call_once。 |
| 在类外静态成员未正确初始化 | 确保所有静态成员在使用前已被定义,或使用局部静态变量。 |
| 拷贝/移动构造函数未删除导致多实例 | 明确删除拷贝/移动构造函数和赋值运算符。 |
C++11 之前的编译器不支持 call_once 或局部静态变量线程安全 |
使用传统的 pthread_once 或自实现的双重检查锁。 |
| 单例在程序终止前未销毁导致资源泄漏 | 如果使用 static 局部变量,编译器会在退出时自动销毁;若使用 new,可在 atexit 注册销毁函数。 |
小结
- Meyers 单例:最简洁,适合 C++11 及以上。
- 双重检查锁:兼容老编译器,但实现难度较高。
call_once:更安全、易读、同样适用于 C++11+。- 智能指针 +
call_once:当需要共享实例生命周期时可采用。
选择哪种实现方式取决于项目的编译环境、性能需求以及对代码可读性的要求。只要遵循“不重复初始化、禁止拷贝/移动、确保线程安全”的三原则,就能得到一个稳健的单例实现。