在多线程环境下实现一个线程安全且懒加载的单例模式,最常用的方式是采用C++11之后的std::call_once和std::once_flag,或者利用局部静态变量的线程安全初始化特性。下面分别展示两种实现方式,并对其优缺点进行分析。
1. 使用 std::call_once
#include <iostream>
#include <mutex>
#include <memory>
class Singleton {
public:
// 获取实例的静态成员函数
static Singleton& instance() {
std::call_once(initFlag, [](){
instancePtr.reset(new Singleton);
});
return *instancePtr;
}
// 其它业务接口
void doSomething() {
std::cout << "Singleton doing something.\n";
}
private:
// 私有构造函数,防止外部直接创建
Singleton() { std::cout << "Singleton constructed.\n"; }
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::once_flag initFlag;
static std::unique_ptr <Singleton> instancePtr;
};
// 静态成员定义
std::once_flag Singleton::initFlag;
std::unique_ptr <Singleton> Singleton::instancePtr = nullptr;
优点
- 线程安全且只初始化一次。
- 通过
std::once_flag与std::call_once的组合,避免了锁竞争。 - 对构造函数进行显式控制,易于定制初始化逻辑。
缺点
- 需要手动维护静态指针,稍显繁琐。
- 若在多进程或 DLL 共享内存环境下使用,需要注意符号导出问题。
2. 利用局部静态变量(C++11 及以后)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后保证线程安全
return instance;
}
void doSomething() {
std::cout << "Singleton doing something.\n";
}
private:
Singleton() { std::cout << "Singleton constructed.\n"; }
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 代码最简洁,几乎不需要手动管理。
- 标准保证局部静态变量在第一次使用时线程安全初始化。
缺点
- 若
Singleton的构造函数抛异常,后续调用instance()仍会重新尝试初始化,可能导致性能问题。 - 对于需要在程序退出时显式销毁对象的场景(例如需要特定顺序销毁全局对象)不够灵活。
3. 何时选用哪种方式?
| 场景 | 推荐实现 |
|---|---|
| 需要在单例初始化时做复杂逻辑,且需要确保仅初始化一次 | std::call_once |
| 单例构造轻量、无异常抛出、且希望代码极简 | 局部静态变量 |
| 需要在多进程或 DLL 中共享同一实例 | 需结合 std::call_once 与合适的符号导出/共享机制 |
4. 小结
在现代 C++(C++11 及以后)中,最常见的实现单例模式的两种方式已经足够满足大多数场景。std::call_once 提供了更细粒度的控制,适合需要自定义初始化流程的复杂单例;而局部静态变量的方式则更加简洁、易于维护。理解两者的内部实现原理可以帮助我们在多线程环境中做出更合理的选择。