在多线程环境下,单例模式需要保证对象只被创建一次,并且所有线程都能安全访问该实例。下面介绍几种常用实现方式,并讨论它们的优缺点。
1. 基于 std::call_once 的实现(C++11+)
#include <iostream>
#include <mutex>
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;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
void doSomething() { std::cout << "Hello from Singleton\n"; }
private:
Singleton() { std::cout << "Singleton constructed\n"; }
~Singleton() = default;
static std::unique_ptr <Singleton> instance;
static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;
优点
- 线程安全,
std::call_once确保初始化块只执行一次。 - 延迟初始化,只有首次调用
getInstance()时才创建对象。 - 代码简洁、易维护。
缺点
std::once_flag的实现依赖底层平台,可能在极少数环境下表现不稳定,但在标准 C++ 环境下已足够稳健。
2. 基于局部静态变量的实现(C++11+)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 起线程安全
return instance;
}
// 其它成员同上
};
优点
- 代码最简洁,直接利用编译器对局部静态的线程安全保证。
- 延迟初始化与
std::call_once兼容。
缺点
- 对于需要在程序结束前显式销毁对象的情况,无法控制销毁顺序,可能导致“静态销毁顺序问题”。
- 在某些编译器中,如果使用了
-fno-threadsafe-statics选项,可能失去线程安全。
3. 双重检查锁(Double-Check Locking)
class Singleton {
public:
static Singleton* getInstance() {
if (!instance) {
std::lock_guard<std::mutex> lock(mtx);
if (!instance) {
instance = new Singleton;
}
}
return instance;
}
private:
static Singleton* instance;
static std::mutex mtx;
};
优点
- 在第一次实例化后,后续调用不再涉及锁,性能更好。
缺点
- 需要对
instance做原子操作,若未使用std::atomic,在某些体系结构上会出现可见性问题。 - 代码复杂度较高,容易出现细节错误,现代 C++ 推荐使用
std::call_once或局部静态变量。
4. Meyers 单例(C++03 兼容)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++03 对静态局部变量的初始化不是线程安全的
return instance;
}
};
优点
- 兼容 C++03,适用于旧编译器。
缺点
- 不是线程安全,需要额外同步机制。
- 与 C++11 版本相比,需手动加锁。
5. 需要注意的细节
| 细节 | 说明 |
|---|---|
| 析构顺序 | 对于单例使用 std::unique_ptr 或局部静态变量时,析构顺序由实现决定,可能导致在其他全局对象析构时使用已被销毁的单例。若不想出现此问题,可采用“懒初始化 + 销毁标志”或将单例设计为“永不过期”。 |
| 异常安全 | std::call_once 的初始化 lambda 必须保证不抛出异常,否则后续调用会再次触发初始化。 |
| 多线程读取 | 单例提供的接口应尽量避免共享可变状态,或者内部使用读写锁、原子变量等保证线程安全。 |
| 性能评估 | 对于高频读访问的单例,建议使用局部静态变量或 std::call_once,避免每次都获取互斥锁。 |
6. 结论
在现代 C++(C++11 及以后)中,最推荐的实现方式是:
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 起线程安全
return instance;
}
// ...
};
或者使用 std::call_once 的显式实现,两者都具备延迟初始化、线程安全、易维护等优点。只有在需要更细粒度控制生命周期或销毁顺序时,才考虑使用更复杂的方案。