在多线程环境下,单例模式的实现必须保证只有一个实例被创建,并且在任何线程里都能安全访问。下面我们从传统实现、C++11 的std::call_once以及使用局部静态变量三种方式,逐步深入探讨。
1. 传统双重检查锁(Double‑Checked Locking)
class Singleton {
public:
static Singleton* getInstance() {
if (instance == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mtx); // 互斥锁
if (instance == nullptr) { // 第二次检查
instance = new Singleton();
}
}
return instance;
}
private:
Singleton() = default;
static Singleton* instance;
static std::mutex mtx;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
关键点
- 双重检查:先不加锁快速返回,只有第一次访问时才需要加锁,减少性能损耗。
- 线程安全:
std::lock_guard保证在作用域结束时自动解锁。 - 懒初始化:实例在首次调用时才创建。
然而,若编译器不遵循C++内存模型,或者在旧的编译器上,instance 的写操作可能未被其他线程看到,导致线程安全问题。
2. C++11 std::call_once 与 std::once_flag
C++11 提供了更可靠的单次初始化机制:
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(flag, []() {
instance.reset(new Singleton());
});
return *instance;
}
private:
Singleton() = default;
static std::unique_ptr <Singleton> instance;
static std::once_flag flag;
};
std::unique_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::flag;
优势
- 原子性:
std::call_once确保闭包只被调用一次。 - 性能:在后续访问时无需锁定。
- 简洁:不必手动维护互斥锁。
3. 局部静态变量(Meyers 单例)
C++11 之后,局部静态变量的初始化是线程安全的。利用这一点,我们可以进一步简化:
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 保证线程安全
return instance;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
注意
- 懒加载:第一次调用
getInstance()时实例才会被创建。 - 不可拷贝/赋值:防止外部复制单例。
- 销毁顺序:实例在程序结束时按逆序销毁,若存在依赖,需谨慎。
4. 线程安全的懒加载与销毁
在多进程/多线程环境中,单例的销毁顺序可能导致“静态销毁顺序问题”。可以通过在 std::shared_ptr 中使用自定义删除器来避免:
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
static std::shared_ptr <Singleton> instance(
new Singleton(),
[](Singleton* ptr) { delete ptr; } // 自定义删除器
);
return instance;
}
// ...
};
5. 总结
| 方法 | 线程安全性 | 性能 | 代码量 |
|---|---|---|---|
| 双重检查锁 | 需要注意内存模型,易错 | 最优(加锁次数少) | 适中 |
std::call_once |
原子安全 | 较好 | 适中 |
| 局部静态变量 | 原子安全 | 最佳 | 简洁 |
在现代 C++(C++11 及以后)项目中,局部静态变量是最推荐的实现方式:代码最简洁,且语言层面已保证线程安全。若需要在类外释放资源或实现更细粒度控制,可考虑 std::call_once 或双重检查锁。
记住:单例模式虽然方便,但过度使用会导致代码耦合度提高,建议只在真正需要全局唯一实例时使用。