在多线程环境下,单例模式常被用来保证某个类只存在一份实例。然而,传统的单例实现往往会出现线程安全问题,甚至导致性能瓶颈。本文将从双重检查锁定(Double‑Check Locking)到C++11中引入的线程安全静态局部变量,详细剖析两种常见的线程安全单例实现,并讨论它们的优缺点与实际应用场景。
1. 双重检查锁定(Double‑Check Locking)
1.1 设计思路
双重检查锁定的核心思想是:在获取实例之前先检查实例是否已创建,若没有则进入临界区;进入临界区后再次检查实例是否已被其他线程创建,若仍未创建则实例化。这样可以在大多数情况下避免每次访问都进行加锁,从而提高性能。
1.2 实现代码
#include <mutex>
#include <atomic>
class Singleton {
public:
static Singleton* instance() {
// 第一次检查(无锁)
Singleton* tmp = instance_;
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
// 第二次检查(加锁)
tmp = instance_;
if (!tmp) {
tmp = new Singleton();
// 使用原子写操作,防止指令重排
instance_.store(tmp, std::memory_order_release);
}
}
return tmp;
}
// 禁止拷贝与赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
// 定义静态成员
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
1.3 关键细节
- 原子指针:使用
std::atomic防止指令重排导致的未初始化访问。 - 内存序:写操作使用
memory_order_release,读操作使用memory_order_acquire。 - 锁的粒度:锁仅在真正创建实例时才持有,减少竞争。
1.4 性能评估
在单线程或少量线程情况下,DCL 具备不错的性能;但在高并发场景中,每次实例化前的非锁检查仍会产生一定开销,且锁竞争可能导致微小延迟。
2. C++11 线程安全的静态局部变量
C++11 标准保证了局部静态变量在首次使用时的初始化是线程安全的。利用这一特性,可以实现非常简洁且性能优越的单例。
2.1 实现代码
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 线程安全初始化
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
2.2 工作原理
- 编译器在生成代码时插入一次性锁,保证同一时间只有一个线程能完成初始化。
- 初始化完成后,其他线程直接返回已有实例,无需加锁。
- 对象的生命周期与程序的生命周期一致,直到程序退出时才被销毁。
2.3 优点与局限
- 优点:实现简洁、无需手动管理锁和原子操作,且性能接近单线程访问。
- 局限:
- 延迟初始化:如果单例在程序早期不被使用,导致的延迟初始化可能影响启动时间。
- 销毁顺序:静态局部变量在程序退出时按逆序销毁,若与其他静态对象交互需小心。
- 缺乏延迟加载的灵活性:无法通过传参或工厂模式灵活创建实例。
3. 何时使用哪种实现?
| 场景 | 推荐实现 | 说明 |
|---|---|---|
| 需要最简洁、可靠、且无额外同步成本 | C++11 静态局部 | 适用于大多数标准应用 |
| 需要手动控制实例生命周期(如在特定时间点销毁) | 双重检查锁定 | 可配合智能指针或手动 delete |
| 需要在多线程环境中提供延迟参数化实例化 | 双重检查锁定 + 智能指针 | 结合工厂模式 |
| 代码必须兼容 C++11 之前的标准 | 双重检查锁定 | 需要自行实现原子和锁机制 |
4. 小结
C++11 通过对局部静态变量的线程安全初始化做出保证,简化了单例模式的实现,使得大多数情况下不需要手写锁和原子操作。然而,双重检查锁定仍然是一个值得学习的模式,尤其是在需要更细粒度控制或兼容旧标准时。理解两种实现的细节与适用场景,能够帮助开发者在实际项目中做出更合适的选择。