在多线程环境下,单例模式的实现往往面临“线程安全”与“性能”两大挑战。本文将从两种常见实现方式:懒汉式(Lazy)和饿汉式(Eager),分别讨论其线程安全的实现细节,并给出完整可编译的示例代码,帮助你在实际项目中快速落地。
1. 懒汉式(Lazy)——按需创建
懒汉式单例的核心是按需创建实例,初始状态下不占用资源。最常见的线程安全实现是使用 std::call_once 配合 std::once_flag:
#include <iostream>
#include <mutex>
class LazySingleton {
public:
static LazySingleton& instance() {
std::call_once(initFlag, []() {
instancePtr.reset(new LazySingleton);
});
return *instancePtr;
}
void sayHello() const { std::cout << "Hello from LazySingleton!\n"; }
private:
LazySingleton() { std::cout << "LazySingleton constructed\n"; }
~LazySingleton() = default;
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
static std::unique_ptr <LazySingleton> instancePtr;
static std::once_flag initFlag;
};
std::unique_ptr <LazySingleton> LazySingleton::instancePtr;
std::once_flag LazySingleton::initFlag;
1.1 关键点说明
| 关键点 | 说明 |
|---|---|
std::once_flag |
只允许一次执行,确保初始化仅发生一次 |
std::call_once |
线程安全地调用一次 lambda 以创建实例 |
unique_ptr |
自动管理单例生命周期,避免手动 delete |
| 拷贝/赋值禁用 | 防止外部复制导致多实例 |
该实现优点是线程安全、延迟加载且无需额外锁,性能接近单线程初始化。
2. 饿汉式(Eager)——静态初始化
饿汉式在程序启动时就完成实例化,天然线程安全(因为 C++11 之后,函数内部静态对象按首次访问时初始化,且初始化是线程安全的)。实现非常简洁:
class EagerSingleton {
public:
static EagerSingleton& instance() {
static EagerSingleton instance;
return instance;
}
void sayHello() const { std::cout << "Hello from EagerSingleton!\n"; }
private:
EagerSingleton() { std::cout << "EagerSingleton constructed\n"; }
~EagerSingleton() = default;
EagerSingleton(const EagerSingleton&) = delete;
EagerSingleton& operator=(const EagerSingleton&) = delete;
};
2.1 何时选用饿汉式?
- 不需要延迟:如果单例本身占用资源不多,或者应用启动时就会使用到,饿汉式更简洁。
- 构造开销小:如果构造函数复杂,且启动期间不会受影响,饿汉式更安全。
3. 结合 C++20 的 consteval 与 constinit
C++20 引入 consteval 与 constinit,可以进一步保证单例在编译期或常量初始化时就完成:
class CompileTimeSingleton {
public:
static constinit CompileTimeSingleton& instance() {
static CompileTimeSingleton inst;
return inst;
}
// ...
private:
CompileTimeSingleton() {}
// ...
};
constinit 保证 instance 的静态存储对象在编译期就完成初始化,避免运行时延迟。
4. 常见错误与陷阱
-
双重检查锁(Double-Check Locking)
旧版 C++ 实现往往使用if (!ptr) { lock(); if (!ptr) ptr = new T; }。若不使用volatile或内存屏障,可能导致未初始化对象被使用。std::call_once是安全且简洁的替代方案。 -
全局静态破坏顺序
单例销毁顺序不确定,若在main结束后仍使用单例,可能出现野指针。建议单例使用std::unique_ptr与std::call_once,或让单例永不过期(如使用constinit)。 -
跨线程的静态成员
在多线程环境中,任何访问单例的函数都必须保证是线程安全的。若单例内部持有可变状态,需使用std::mutex或std::atomic进行同步。
5. 小结
- 懒汉式:使用
std::call_once或std::atomic以确保一次性初始化,适合延迟加载场景。 - 饿汉式:利用 C++11 对局部静态的线程安全初始化特性,代码最简洁,适合无延迟需求。
- C++20:
constinit与consteval可进一步提升编译期安全性。
根据实际需求(资源占用、延迟、线程安全保障程度)选择合适的实现方式即可。祝你在 C++ 项目中顺利使用单例模式,构建稳健且高效的代码架构!