单例模式(Singleton Pattern)是一种常用的设计模式,用于保证某个类在整个程序中只产生一次实例,并提供全局访问点。在多线程环境下,若不加以控制,可能导致多线程同时创建多个实例,破坏单例性质。下面介绍几种在C++中实现线程安全单例的常见方法,并分析其优缺点。
1. 经典的 Meyers 单例(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() = default;
~Singleton() = default;
};
原理
- 通过函数内部的
static局部变量实现懒加载(第一次调用时才创建实例)。 - C++11 规定,局部静态变量的初始化是原子操作,且只执行一次,因此即使多个线程同时进入
instance(),也只会产生一个实例。
优点
- 代码简洁,易于维护。
- 无需手动加锁,避免了锁竞争和死锁风险。
- 延迟加载,首次使用才创建实例,节省资源。
缺点
- 对旧编译器(C++03)不兼容。
- 若想在单例销毁前做特定操作(如日志),需要额外设计。
2. 双重检查锁(Double-Check Locking)
class Singleton {
public:
static Singleton* instance() {
if (!ptr_) { // 第一次检查(无锁)
std::lock_guard<std::mutex> lock(mutex_);
if (!ptr_) { // 第二次检查(加锁)
ptr_ = new Singleton();
}
}
return ptr_;
}
// 同样删除拷贝构造与赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::atomic<Singleton*> ptr_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::ptr_{nullptr};
std::mutex Singleton::mutex_;
原理
- 首先无锁检查实例是否已存在,若不存在则获取互斥锁,再检查一次后创建实例。
- 通过
std::atomic保证多线程可见性。
优点
- 在 C++03 或不支持 C++11 的环境中可用。
- 只在实例首次创建时加锁,后续调用不受锁影响。
缺点
- 代码相对复杂,容易出错(例如忘记
atomic或锁)。 - 对构造函数异常的处理不够优雅,可能导致
ptr_未被正确释放。
3. 静态类成员初始化(早期初始化)
class Singleton {
public:
static Singleton& instance() {
return *ptr_;
}
private:
Singleton() = default;
~Singleton() = default;
static Singleton* ptr_;
};
Singleton* Singleton::ptr_ = new Singleton();
原理
- 通过在类外部静态成员指针初始化,保证在程序启动时创建实例。
优点
- 实现简单,编译器负责初始化顺序。
缺点
- 实例在程序启动时即创建,不能实现懒加载。
- 在多模块编译时可能出现“初始化顺序问题”,导致跨模块访问时
ptr_未初始化。
4. 用 std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, [](){ ptr_ = new Singleton(); });
return *ptr_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* ptr_;
static std::once_flag flag_;
};
Singleton* Singleton::ptr_ = nullptr;
std::once_flag Singleton::flag_;
原理
std::call_once确保给定 lambda 只被调用一次,即使有多个线程同时请求。std::once_flag用于记录调用状态。
优点
- 兼容 C++11,支持懒加载。
- 线程安全且实现简单,避免了手动锁。
缺点
- 同样需要手动管理
ptr_的生命周期(如在atexit时删除),否则可能导致资源泄漏。
5. 现代 C++ 推荐:Meyers 单例
综上所述,在支持 C++11 及以后标准的项目中,Meyers 单例 是最推荐的实现方式,因为:
- 简洁:仅一行
static变量。 - 安全:编译器保证线程安全。
- 延迟加载:首次访问时才实例化。
- 可维护:不需要显式锁,代码更易读。
如果你需要在 C++03 环境下实现,或者对单例销毁时的顺序有特殊要求,可以考虑 std::call_once 或双重检查锁方案。
6. 小结
| 方法 | 适用标准 | 延迟加载 | 线程安全实现方式 |
|---|---|---|---|
| Meyers 单例 | C++11+ | ✔ | 编译器保证 |
| 双重检查锁 | C++11+ | ✔ | 互斥锁 + 原子 |
| 静态成员初始化 | 任何 | ✘ | 程序启动时 |
| std::call_once | C++11+ | ✔ | once_flag |
在实际项目中,请根据编译环境、性能需求以及资源管理需求选择最合适的实现方式。祝编码愉快!