在现代软件开发中,单例模式是一种常用的设计模式,旨在确保某个类只有一个实例,并提供全局访问点。对于需要在多线程环境下使用的单例,线程安全性尤为重要。本文将介绍几种在C++17及以后标准中实现线程安全单例的常见方法,并比较它们的优缺点。
1. C++11 的局部静态变量实现(懒汉式)
自C++11起,局部静态变量的初始化是线程安全的。我们可以直接利用这一特性实现单例:
class Logger {
public:
static Logger& getInstance() {
static Logger instance; // 线程安全的懒汉式
return instance;
}
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mtx_);
std::cout << msg << std::endl;
}
private:
Logger() = default;
std::mutex mtx_;
};
优点
- 代码简洁,易于维护。
- 延迟初始化,直到真正需要时才创建实例。
- 线程安全性由语言标准保证,开发者不必手动处理。
缺点
- 无法控制实例的销毁时机,程序结束时会自动销毁。
- 若需要在构造时执行复杂逻辑,异常处理可能更难。
2. Meyers 单例(静态局部+std::call_once)
如果想在单例第一次使用时执行一次初始化逻辑,可以结合 std::call_once:
class Config {
public:
static Config& instance() {
std::call_once(initFlag_, [](){
instance_.reset(new Config);
});
return *instance_;
}
private:
Config() { /* 复杂初始化 */ }
static std::unique_ptr <Config> instance_;
static std::once_flag initFlag_;
};
std::unique_ptr <Config> Config::instance_;
std::once_flag Config::initFlag_;
优点
- 对实例化过程进行更细粒度的控制。
- 能够在构造时抛出异常,且不影响全局单例的使用。
缺点
- 代码相对繁琐。
- 若实例化过程非常耗时,仍可能导致首次调用阻塞。
3. 双重检查锁(双重检锁)+ std::atomic
在 C++11 以前,双重检查锁(Double-Checked Locking)是实现懒汉式单例的经典方法,但由于内存模型问题存在安全隐患。C++11 之后结合 std::atomic 可以安全实现:
class Resource {
public:
static Resource* getInstance() {
Resource* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Resource();
instance_.store(tmp, std::memory_order_release);
}
}
return tmp;
}
private:
Resource() = default;
static std::atomic<Resource*> instance_;
static std::mutex mtx_;
};
std::atomic<Resource*> Resource::instance_{nullptr};
std::mutex Resource::mtx_;
优点
- 只在第一次实例化时才加锁,后续调用几乎无开销。
- 线程安全且可读性较好。
缺点
- 需要手动管理实例的生命周期(如在
atexit时删除)。 - 代码较为复杂,容易出现细节错误。
4. 静态类成员与 std::shared_ptr 的组合
如果单例需要被多处共享,并且需要自动销毁,可以使用 std::shared_ptr:
class Cache {
public:
static std::shared_ptr <Cache> getInstance() {
static std::shared_ptr <Cache> instance(new Cache, [](Cache* p){ delete p; });
return instance;
}
private:
Cache() = default;
};
auto c = Cache::getInstance(); // 可以多次复制引用
优点
- 支持多引用计数,灵活的资源管理。
- 自动销毁,避免内存泄漏。
缺点
- 每次访问需要
shared_ptr的拷贝开销。 - 对性能敏感的场景不适用。
5. 综述与最佳实践
| 实现方式 | 延迟初始化 | 线程安全 | 代码复杂度 | 生命周期控制 | 适用场景 |
|---|---|---|---|---|---|
| 静态局部变量 | ✔ | ✔ | 低 | 结束时销毁 | 简单单例 |
call_once |
✔ | ✔ | 中 | 结束时销毁 | 初始化复杂 |
| 双重检查锁 | ✔ | ✔ | 高 | 手动 | 性能关键 |
shared_ptr |
✔ | ✔ | 中 | 自动 | 需要共享计数 |
推荐
- 对大多数业务场景,使用 C++11 的静态局部变量即可满足需求。
- 若需要在单例构造时执行异常安全的初始化,结合
std::call_once更为稳妥。 - 性能极端要求的项目可考虑双重检查锁,但请务必在 C++11 之后使用
std::atomic确保正确性。
6. 小结
线程安全的单例是 C++ 并发编程中的基础技术之一。通过充分利用 C++11 及以后标准提供的语言特性(如局部静态变量、std::call_once、std::atomic、std::shared_ptr),我们可以轻松实现既简洁又安全的单例。关键在于根据项目需求权衡初始化复杂度、性能和生命周期管理,选择最合适的实现方式。祝编码愉快!