在多线程环境中实现单例模式时,最关键的是保证实例在第一次使用时只被创建一次,同时避免多线程竞争导致的重复实例化。C++11 之后的标准提供了多种原子性和同步机制,可以轻松实现线程安全的单例。
1. 经典实现:双重检查锁(Double-Checked Locking)
双重检查锁的核心思路是在判断实例是否存在时使用两次锁定,第一次检查不加锁,第二次检查加锁。它的缺点是实现相对复杂,并且在一些编译器中仍可能出现可见性问题。
class Singleton {
public:
static Singleton& instance() {
if (instance_ == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr) { // 第二次检查
instance_ = new Singleton();
}
}
return *instance_;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
~Singleton() {
delete instance_;
instance_ = nullptr;
}
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
2. C++11 本地静态变量实现
从 C++11 开始,编译器保证对局部静态变量的初始化是线程安全的。只需在函数内部定义静态对象即可:
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 线程安全初始化
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
};
该实现最简洁,且性能优秀。局部静态变量在第一次调用 instance() 时才会被构造,随后所有线程共享同一实例。
3. std::call_once 方案
std::call_once 与 std::once_flag 组合使用,可以在多线程环境下确保某段代码仅执行一次。它常用于需要延迟初始化且实例类型不支持局部静态的情况。
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, []() {
instance_ = new Singleton();
});
return *instance_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
~Singleton() { delete instance_; }
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
4. 何时使用哪种实现?
| 场景 | 推荐实现 | 原因 |
|---|---|---|
| 需要兼容 C++98/C++03 | 双重检查锁(配合编译器的内存屏障) | 无法使用 C++11 新特性 |
| 需要简洁、安全、延迟初始化 | 局部静态变量 | C++11 标准保证线程安全 |
| 需要对对象的创建过程进行自定义(如抛异常、日志) | std::call_once |
允许使用 lambda 或自定义函数 |
5. 注意事项
- 单例的析构:若在程序退出前需要销毁单例,必须显式 delete。使用局部静态时,析构会在程序结束时自动调用。
std::call_once方式需要手动管理。 - 避免全局对象的构造顺序问题:如果单例依赖于其他全局对象,可能出现依赖顺序问题。建议将单例的实现延迟到真正需要时。
- 线程安全性:即使使用局部静态,仍需确保单例内部的成员函数在多线程环境下也是线程安全的。可通过内部锁或原子操作实现。
通过上述三种方式,C++ 开发者可以根据项目的编译环境、性能需求以及实现复杂度,选择最适合的线程安全单例实现。