在多线程环境下,单例模式的实现需要保证以下两点:
- 懒初始化:只有在第一次使用时才创建实例。
- 线程安全:在多线程同时访问时,不能产生多个实例,也不能出现竞态条件。
下面给出几种常见的实现方式,并对它们的优缺点进行分析。
1. 使用 std::call_once 与 std::once_flag
#include <mutex>
#include <memory>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, [](){
instancePtr_ = std::unique_ptr <Singleton>(new Singleton);
});
return *instancePtr_;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() { /* 可能的初始化工作 */ }
~Singleton() = default;
static std::once_flag initFlag_;
static std::unique_ptr <Singleton> instancePtr_;
};
std::once_flag Singleton::initFlag_;
std::unique_ptr <Singleton> Singleton::instancePtr_;
优点
- 代码简洁,标准库提供的
std::call_once在 C++11 之后已经被优化为高效、线程安全的实现。 - 避免了手动的双重检查锁(double‑check locking)模式的陷阱。
缺点
- 在编译器不完全支持 C++11 的环境下无法使用。
instancePtr_是智能指针,析构时会自动释放;如果想手动控制销毁时机,需要额外处理。
2. 局部静态变量(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() { /* 可能的初始化工作 */ }
~Singleton() = default;
};
优点
- 语法极其简洁,几行代码即可完成。
- C++11 标准保证局部静态变量在第一次使用时线程安全地初始化。
- 无需手动管理内存,天然支持销毁。
缺点
- 对编译器的标准实现要求较高,旧版本编译器可能不支持。
- 如果实例需要按特定顺序销毁,可能需要自行控制。
3. 双重检查锁(传统实现,需注意细节)
#include <mutex>
class Singleton {
public:
static Singleton* instance() {
if (ptr_ == nullptr) { // 第一检查
std::lock_guard<std::mutex> lock(mutex_);
if (ptr_ == nullptr) { // 第二检查
ptr_ = new Singleton;
}
}
return ptr_;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* ptr_;
static std::mutex mutex_;
};
Singleton* Singleton::ptr_ = nullptr;
std::mutex Singleton::mutex_;
优点
- 与
std::call_once类似,能够避免无谓的锁开销。
缺点
- 需要确保
Singleton的构造函数是可见的且没有副作用。 - 在旧编译器或不规范的实现中可能出现 指令重排 导致线程看到未完全构造的对象。
- 需要手动管理
ptr_的销毁,容易出现内存泄漏。
4. Meyer’s Singleton(编译器实现差异)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 通过编译器实现决定线程安全
return instance;
}
// ...
};
该实现依赖编译器对局部静态变量初始化的线程安全保证。C++11 标准强制要求线程安全,但在 C++98/03 仍需手动同步。
5. 如何选择?
| 实现方式 | 适用场景 | 主要优点 | 主要缺点 |
|---|---|---|---|
std::call_once |
需要自定义销毁时机 | 代码清晰、线程安全 | 需要 C++11 |
| 局部静态变量 | 最简洁、自动销毁 | C++11 标准保证 | 旧编译器不支持 |
| 双重检查锁 | 旧编译器或特定平台 | 减少锁开销 | 易出错、指令重排 |
| Meyer’s Singleton | 简易实现 | 纯粹 C++11 | 取决编译器 |
- 如果你使用的是 C++11 及以上,推荐使用局部静态变量或者
std::call_once的组合。 - 若对销毁时机有严格要求,可考虑
std::call_once加std::unique_ptr。 - 在旧编译器(如 C++98/03)环境下,使用双重检查锁时务必保证 内存屏障 或 volatile 的正确使用,或者直接采用第三方线程库实现。
6. 小结
实现线程安全单例并不是一件难事,只要把握好以下几点:
- 只在第一次使用时创建:懒加载是单例的核心。
- 避免重复初始化:双重检查锁或
std::call_once可以保证这一点。 - 保证构造与销毁的原子性:使用标准库的同步工具能大幅降低出错概率。
- 避免不必要的锁:局部静态变量在 C++11 之后已保证线程安全,使用时不需要再手动加锁。
掌握这些基本原则后,你就能在任何 C++ 项目中安全、简洁地实现单例模式。祝编码愉快!