单例模式(Singleton)是一种常用的软件设计模式,确保一个类在整个程序生命周期内仅有一个实例,并提供全局访问点。在多线程环境下,如何保证单例的创建过程是线程安全的,是实现该模式时需要重点考虑的问题。下面从 C++11 及之后的标准入手,介绍几种常见的线程安全实现方案,并给出完整示例代码。
1. C++11 的 std::call_once + std::once_flag
C++11 标准库提供了 std::call_once 和 std::once_flag,专门用于一次性初始化。其内部实现采用了原子操作和互斥锁,能够在多线程环境下确保只执行一次初始化代码。
#include <iostream>
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, []() {
instance_ = new Singleton();
});
return *instance_;
}
void doSomething() const { std::cout << "Singleton instance address: " << this << '\n'; }
private:
Singleton() { std::cout << "Constructor called\n"; }
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
优点
- 代码简洁,易于维护
- 线程安全,避免了手动使用互斥锁导致的死锁或性能瓶颈
缺点
std::call_once的实现可能在某些编译器或平台上存在性能差异,需根据实际需求评估。
2. 局部静态变量(Meyers Singleton)
C++11 之后,函数内部的局部静态变量初始化是线程安全的。该实现方式最为简洁,且无需显式使用互斥锁。
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 线程安全初始化
return instance;
}
void doSomething() const { std::cout << "Singleton instance address: " << this << '\n'; }
private:
Singleton() { std::cout << "Constructor called\n"; }
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 极简代码,避免手动管理内存
- C++11 标准保证线程安全
缺点
- 对象生命周期始终与程序生命周期绑定,无法在需要时销毁
- 可能导致编译时静态构造函数的异常传播问题(虽然在 C++11 之后已得到改进)。
3. 双重检查锁(Double-Check Locking)
在 C++11 之前,双重检查锁是实现线程安全单例的常用手段。但在 C++11 之后,由于内存模型的改变,若未使用 std::atomic 或 std::mutex,可能导致出现 “脏读” 的问题。因此若坚持使用此模式,需确保使用 std::atomic 或 std::mutex。
#include <atomic>
#include <mutex>
class Singleton {
public:
static Singleton* instance() {
Singleton* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton();
instance_.store(tmp, std::memory_order_release);
}
}
return tmp;
}
void doSomething() const { std::cout << "Singleton instance address: " << this << '\n'; }
private:
Singleton() { std::cout << "Constructor called\n"; }
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
优点
- 在已创建实例后,后续访问不需要锁,提升性能
缺点
- 代码复杂,易出错
- 需要额外关注内存排序和同步细节。
4. 总结与建议
| 实现方式 | 代码量 | 线程安全 | 生命周期控制 | 适用场景 |
|---|---|---|---|---|
std::call_once |
中等 | ✔ | 程序结束时销毁 | 需要手动销毁或延迟初始化 |
| 局部静态变量 | 极简 | ✔ | 程序结束时销毁 | 简单场景,生命周期与程序一致 |
| 双重检查锁 | 复杂 | ✔ (需 careful) | 程序结束时销毁 | 对性能极端敏感且旧标准支持 |
在现代 C++ 开发中,推荐使用局部静态变量 或 std::call_once 的实现方式。它们都具备线程安全、易于维护、性能足够好,并且符合 C++11 及之后的标准。
如果你在使用某些老旧编译器(如 MSVC 2015 之前)或需要在全局作用域中提前销毁实例,请优先考虑 std::call_once 方案。
实践小贴士
- 为避免多线程竞争导致的“僵尸”实例,请确保在
main结束前不再引用单例,或使用std::shared_ptr与自定义deleter来管理销毁。 - 对于大型项目,考虑使用 依赖注入 或 服务定位器 替代传统单例,提升模块化与可测试性。