在多线程环境下实现线程安全的单例模式,一直是 C++ 开发者关注的热点。传统的懒汉式单例需要显式的加锁,容易出现性能瓶颈或死锁;而 Eager 单例虽然线程安全但缺乏懒加载特性。幸运的是,从 C++11 开始,标准库提供了一些工具,使得实现既简洁又安全。下面给出几种主流方案,并说明各自的优缺点。
1. Meyers 单例(C++11 之后)
class Singleton {
public:
static Singleton& instance() {
static Singleton obj; // 函数内部的静态局部对象
return obj;
}
// 禁止拷贝与赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
void doSomething() {
// ...
}
private:
Singleton() = default; // 私有构造函数
~Singleton() = default;
};
- 原理:C++11 规定局部静态对象在第一次调用时会进行线程安全的初始化(实现保证);
- 优点:代码最短,天然线程安全,无需显式锁;
- 缺点:若构造函数抛异常,
instance()需要再次调用;若想延迟销毁(C++17 的std::unique_ptr+std::atexit)需额外处理。
2. std::call_once + std::once_flag
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, []() {
instancePtr.reset(new Singleton);
});
return *instancePtr;
}
// 禁止拷贝与赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::unique_ptr <Singleton> instancePtr;
static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;
- 原理:
std::call_once保证闭包只执行一次,std::once_flag是其同步原语; - 优点:构造过程可以更灵活(如使用
std::unique_ptr或自定义销毁顺序); - 缺点:比 Meyers 方案略繁琐,仍需要手动管理销毁。
3. 延迟销毁的 std::shared_ptr + std::weak_ptr
如果你想让单例在程序结束前自动销毁,而不是依赖静态对象的析构顺序,可以使用 shared_ptr:
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::call_once(initFlag, []() {
instancePtr = std::shared_ptr <Singleton>(new Singleton);
});
return instancePtr;
}
// 其余同上…
private:
Singleton() = default;
static std::shared_ptr <Singleton> instancePtr;
static std::once_flag initFlag;
};
std::shared_ptr <Singleton> Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;
- 优点:
shared_ptr的析构会在全局静态对象销毁之前完成,避免了“静态析构顺序问题”; - 缺点:额外的引用计数开销,且如果出现循环引用需要手动打破。
4. 对比与实践建议
| 方案 | 初始化方式 | 线程安全 | 析毁顺序 | 代码复杂度 |
|---|---|---|---|---|
| Meyers | C++11 局部静态 | ✅ | 可能存在顺序问题 | ★ |
call_once + unique_ptr |
显式单次初始化 | ✅ | 可自定义 | ★★ |
call_once + shared_ptr |
显式单次初始化 | ✅ | 自动 | ★★ |
建议:
- 对于大多数项目,只要单例不需要特殊销毁,Meyers 单例 即可满足需求,代码最简洁;
- 如果你需要在析构前做清理或想避免静态析构顺序问题,采用
call_once+shared_ptr更为稳妥; - 对于极少数需要复杂初始化逻辑或需要在多线程中动态切换实例的情况,可使用
call_once+unique_ptr并结合工厂模式。
5. 小结
C++11 以后,线程安全单例的实现已经不再需要手动加锁。选择合适的方案取决于你对初始化时机、销毁顺序以及代码复杂度的需求。掌握这两种核心技术(Meyers 和 std::call_once),你就能在任何 C++ 项目中灵活、可靠地使用单例模式。祝编码愉快!