单例模式(Singleton)是一种常用的软件设计模式,用于保证一个类在整个程序生命周期内只有一个实例,并提供全局访问点。随着多线程程序的兴起,传统的单例实现往往在并发环境下出现竞争问题,导致产生多个实例或访问不安全。下面将从理论与实践两方面,介绍几种在C++17及以上版本中实现线程安全单例的方案,并对比它们的优缺点。
1. 理论基础
1.1 单例的核心需求
- 唯一性:全局只能有一个实例。
- 懒加载:实例在第一次使用时才创建(可选)。
- 线程安全:多线程并发访问时不产生竞态条件。
- 全局访问:通过静态方法或全局对象访问。
1.2 C++的并发原语
std::call_once与std::once_flagstd::mutex与std::lock_guard- 原子操作
std::atomic
2. 实现方案
2.1 使用 std::call_once(推荐)
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, []{
instance.reset(new Singleton);
});
return *instance;
}
// 业务方法示例
void doWork() { std::cout << "工作中...\n"; }
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::unique_ptr <Singleton> instance;
static std::once_flag initFlag;
};
// 需要在编译单元中定义静态成员
std::unique_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::initFlag;
优点
- 线程安全且高效。
- 懒加载(仅在第一次调用时创建)。
- 代码简洁,易于维护。
缺点
- 需要在别的翻译单元中定义静态成员。
2.2 局部静态变量(C++11之后)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 保证线程安全的局部静态初始化
return instance;
}
// ... 其它成员
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 代码极简,完全不需要手动管理静态成员。
- C++11标准保证了线程安全的局部静态初始化。
缺点
- 如果
Singleton的构造函数抛异常,后续调用会再次尝试初始化。 - 需要确保编译器符合C++11的实现规范。
2.3 双重检查锁(DCLP)
class Singleton {
public:
static Singleton* getInstance() {
Singleton* 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 Singleton;
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
// ...
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::atomic<Singleton*> instance;
static std::mutex mtx;
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;
优点
- 通过原子指针实现懒加载,锁粒度小。
缺点
- 代码复杂,易出现错误。
- 需要手动管理内存,存在泄露风险。
3. 何时选择哪种实现?
| 场景 | 推荐实现 |
|---|---|
| 简单项目,易读性优先 | 局部静态变量 |
| 需要显式控制销毁或支持多进程 | std::call_once + unique_ptr |
| 高性能对锁开销极致敏感 | 双重检查锁(慎用) |
4. 常见陷阱与调试技巧
- 静态成员初始化顺序:若单例依赖其他全局对象,需避免“静态初始化顺序悖论”。
- 多线程异常:若构造函数抛异常,
std::call_once会抛异常,后续再次调用仍会尝试创建实例。 - 删除拷贝构造/赋值:确保单例不可复制。
- 线程局部存储:若单例需要线程局部状态,可在内部使用
thread_local变量。
5. 结语
C++11以后提供了强大的并发原语,使得实现线程安全单例变得既简洁又可靠。对于大多数应用场景,std::call_once 与局部静态变量 已足够满足需求。更复杂的情形下,可根据性能与内存管理需求,选择双重检查锁或自定义原子操作。希望本文能帮助你在多线程项目中稳健地使用单例模式。