在 C++ 中实现单例(Singleton)模式时,常见的挑战之一就是确保在多线程环境下只有一个实例被创建,并且该实例在整个程序生命周期内保持唯一。下面给出几种常用且线程安全的实现方式,并说明各自的优缺点。
1. 本地静态变量(Meyers Singleton)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 之后保证线程安全
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {} // 构造函数私有化
};
优点
- 简洁、易于理解。
- C++11 起,
static变量在第一次使用时的初始化已被保证为线程安全(编译器会生成适当的锁)。
缺点
- 如果在程序结束时需要显式销毁单例,C++ 标准不允许直接控制析构时机;可能导致资源在析构前被提前释放。
- 在某些早期编译器(C++11 之前)可能不安全,需要额外的同步机制。
2. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, [](){
instance.reset(new Singleton);
});
return *instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
static std::unique_ptr <Singleton> instance;
static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::initFlag;
优点
- 明确指定初始化函数,能在多线程环境下安全执行一次。
- 可与
unique_ptr结合,方便后期资源管理。
缺点
- 需要额外的头文件 ` `,代码略显繁琐。
- 对于
static成员的使用,仍需要手动定义外部存储。
3. 双重检查锁(Double-Check Locking)
class Singleton {
public:
static Singleton* getInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
if (instance == nullptr) {
instance = new Singleton;
}
}
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {}
static Singleton* instance;
static std::mutex mtx;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
优点
- 只在第一次访问时加锁,性能相对较好。
缺点
- 在 C++ 之前的标准中,因内存可见性问题(写缓冲)会导致错误;需要使用
std::atomic<Singleton*>或std::memory_order。 - 代码比较繁琐,易出错,通常不推荐。
4. 枚举实现(C++11 之后)
class Singleton {
public:
static Singleton& getInstance() {
enum { SingletonEnabler = 0 };
static Singleton instance(SingletonEnabler);
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
explicit Singleton(int) {}
};
优点
- 通过枚举参数让构造函数成为私有但允许在类内部调用,保持单例实例的唯一性。
缺点
- 代码不够直观,对新手友好度低。
何时使用哪种实现?
| 场景 | 推荐实现 |
|---|---|
| 需要最小代码量且使用 C++11 以上 | 本地静态变量(Meyers) |
需要显式控制初始化时机(如在 main 入口前初始化) |
std::call_once |
| 在多线程初始化成本高且只能在一次访问时加锁 | 双重检查锁(仅在 C++11 以上并使用原子操作) |
需要在编译期确定单例(如使用 enum) |
枚举实现 |
小结
在 C++ 中实现线程安全的单例并不复杂。最推荐的方式是使用 Meyers Singleton(本地静态变量)——代码简洁,且从 C++11 开始已保证线程安全;如果需要更细粒度的控制,可使用 std::call_once。双重检查锁和枚举实现多用于特殊需求,但不宜作为首选。
通过以上几种实现方式,你可以根据项目需求、编译器标准和性能要求,选择最合适的单例模式实现。祝编码愉快!