在多线程环境下,单例模式往往需要保证一次且仅一次的实例化。传统的双重检查锁定(Double-Checked Locking)实现容易出现竞态条件,导致实例被多次创建。幸运的是,自 C++11 起,语言本身提供了几种安全、简洁的实现方式,下面将分别介绍并比较它们的优缺点。
1. 使用 std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, [](){
instancePtr = new Singleton();
});
return *instancePtr;
}
// 禁止拷贝和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instancePtr;
static std::once_flag initFlag;
};
Singleton* Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;
- 优点:线程安全,且只初始化一次;实现简洁。
- 缺点:手动管理内存,可能导致程序退出时未释放资源(虽然大多数操作系统会回收内存)。
2. 函数内部静态变量(局部静态变量)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后保证线程安全
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
- 优点:最简洁,编译器保证线程安全,且实例在程序结束时自动析构。
- 缺点:如果你需要在特定顺序销毁对象,或者想手动控制生命周期,则不够灵活。
3. std::shared_ptr + std::atomic
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
auto ptr = instancePtr.load();
if (!ptr) {
std::lock_guard<std::mutex> lock(mtx);
ptr = instancePtr.load();
if (!ptr) {
ptr = std::shared_ptr <Singleton>(new Singleton());
instancePtr.store(ptr);
}
}
return ptr;
}
// 其余与 1 同
private:
Singleton() = default;
static std::atomic<std::shared_ptr<Singleton>> instancePtr;
static std::mutex mtx;
};
std::atomic<std::shared_ptr<Singleton>> Singleton::instancePtr{nullptr};
std::mutex Singleton::mtx;
- 优点:允许多线程共享实例,同时支持在实例被销毁后重新创建(若需要)。对某些库的引用计数管理友好。
- 缺点:实现稍复杂;若不需要共享计数,使用
std::shared_ptr可能是过度设计。
4. 经典 Meyers 单例(懒加载 + 静态局部)
这其实就是上面第二种方式,但值得再次强调它的“Meyers”名字源于 Scott Meyers,强调了它的优雅与安全性。
5. 对比与选择
| 方法 | 线程安全 | 内存泄漏风险 | 生命周期控制 | 代码复杂度 |
|---|---|---|---|---|
std::call_once |
✅ | 需要手动 delete | ❌ | 中等 |
| 局部静态变量 | ✅ | 无 | ✅ | 低 |
std::shared_ptr + atomic |
✅ | 无 | ✅ | 高 |
| Meyers 单例 | ✅ | 无 | ✅ | 低 |
- 如果你只需要一个简单、可靠、自动销毁的单例,使用局部静态变量(Meyers 单例)是最推荐的做法。
- 如果你需要在多线程环境中保证单次初始化,并且想手动控制对象生命周期,
std::call_once+std::once_flag是最佳选择。 - 如果你想让单例可以被多次销毁和重建,或者需要共享计数,可以考虑
std::shared_ptr+std::atomic方案。
6. 常见错误示例
// 错误:未使用 std::once_flag,导致多次初始化
class BadSingleton {
public:
static BadSingleton& instance() {
if (!ptr) { // 线程不安全
ptr = new BadSingleton();
}
return *ptr;
}
// …
private:
static BadSingleton* ptr;
};
此实现会在并发访问时导致 ptr 被多次赋值,甚至出现双重构造。务必使用 std::call_once 或局部静态变量。
7. 结语
C++11 以后,单例模式的实现变得更加安全、简洁。大多数情况下,局部静态变量已足够满足需求;若需特殊控制,std::call_once 与 std::once_flag 提供了灵活的选择。掌握这些工具,你就能在多线程项目中放心使用单例,而不必担心竞态条件或内存泄漏。