在 C++11 标准以后,标准库提供了更安全、更简洁的单例实现方法。
下面我们先讨论传统的双检锁实现与其缺陷,然后给出基于 std::call_once 的最佳实践。
1. 传统双检锁(Double-Checked Locking)
class Singleton {
private:
static Singleton* instance;
static std::mutex mtx;
Singleton() = default;
public:
static Singleton* getInstance() {
if (!instance) { // 第一次检查
std::lock_guard<std::mutex> lock(mtx);
if (!instance) { // 第二次检查
instance = new Singleton();
}
}
return instance;
}
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
问题
- 内存可见性:在多线程环境下,写操作
instance = new Singleton()可能在其他线程看到之前,完成构造函数的部分工作。 - 指令重排:编译器或 CPU 可能会把对象构造的步骤与指针赋值顺序打乱。
- 性能瓶颈:每次调用都需要检查
instance,即使对象已经存在,也会有额外的判断开销。
2. 使用 std::call_once 与 std::once_flag
C++11 标准提供了 std::call_once 机制,确保某个函数只被执行一次,且对所有线程可见。
class Singleton {
private:
Singleton() = default;
static Singleton* instance;
static std::once_flag initFlag;
static void initSingleton() {
instance = new Singleton();
}
public:
static Singleton* getInstance() {
std::call_once(initFlag, initSingleton);
return instance;
}
};
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;
优点
- 简洁安全:
call_once内部使用原子操作,避免了双检锁带来的可见性问题。 - 延迟加载:仅在第一次请求时创建实例,符合懒汉式单例。
- 可扩展:如果你想把初始化逻辑放到
initSingleton中,保持线程安全。
3. 现代 C++:局部静态变量(Meyers 单例)
最简洁且线程安全的实现是利用 C++11 对局部静态变量的初始化是线程安全的特性:
class Singleton {
private:
Singleton() = default;
public:
static Singleton& instance() {
static Singleton inst; // C++11 起线程安全
return inst;
}
};
优点:
- 无显式锁:编译器自动处理同步。
- 资源释放:
inst会在程序结束时析构,符合 RAII。 - 使用更直观:返回引用,避免了指针操作。
4. 何时选择哪种实现?
| 场景 | 推荐实现 |
|---|---|
| 需要在单例中执行复杂初始化逻辑,且想把初始化拆成多个步骤 | std::call_once |
| 只需要一个极简实现,且不想自己写锁 | 局部静态变量(Meyers) |
| 兼容 C++11 前的编译器 | 双检锁(但要注意实现细节) |
5. 小结
C++11 之后,单例模式的实现变得更安全、更简洁。
std::call_once提供了线程安全的显式初始化方式。Meyers单例(局部静态变量)是最简洁、易维护的选择。
在实际项目中,除非有特殊需求,一般推荐使用局部静态变量实现,保持代码简洁且线程安全。