在多线程环境下,单例(Singleton)模式需要保证:
- 只创建一次实例;
- 对所有线程可见;
- 初始化过程是原子性的。
下面给出几种常用实现方式,并说明其优缺点。
1. 采用Meyers Singleton(局部静态变量)
class ThreadSafeSingleton {
public:
static ThreadSafeSingleton& instance() {
static ThreadSafeSingleton inst; // C++11之后保证线程安全
return inst;
}
// 禁止拷贝构造和赋值
ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
void doSomething() {
// 业务逻辑
}
private:
ThreadSafeSingleton() { /* 可能的初始化 */ }
};
优点
- 代码最简洁,几乎不需要手动同步。
- 对象在第一次调用
instance()时才创建,支持懒加载。 - C++标准保证局部静态变量的初始化是线程安全的(C++11及以后)。
缺点
- 如果实例构造失败,后续调用会重复尝试,导致异常抛出。
- 在某些编译器/标准库实现中,初始化过程中出现竞态可能导致性能下降(虽然理论上是安全的)。
2. 双重检查锁(Double‑Checked Locking)
#include <mutex>
class DCLSingleton {
public:
static DCLSingleton* getInstance() {
if (!ptr) { // 第一次检查(无锁)
std::lock_guard<std::mutex> lock(mtx);
if (!ptr) { // 第二次检查(加锁)
ptr = new DCLSingleton();
}
}
return ptr;
}
private:
DCLSingleton() {}
~DCLSingleton() {}
DCLSingleton(const DCLSingleton&) = delete;
DCLSingleton& operator=(const DCLSingleton&) = delete;
static DCLSingleton* ptr;
static std::mutex mtx;
};
DCLSingleton* DCLSingleton::ptr = nullptr;
std::mutex DCLSingleton::mtx;
优点
- 对于多线程并发请求,只有第一次实例化时才加锁,后续请求几乎无锁。
缺点
- 需要显式地使用
mutex,容易出现忘记加锁或误删锁的错误。 - 在旧标准(C++03)下,因内存可见性问题会出现“指针可见但未初始化”的错误。
- 现代C++下推荐使用
Meyers Singleton,更安全简洁。
3. 静态局部对象 + std::call_once(更显式控制)
#include <mutex>
class OnceSingleton {
public:
static OnceSingleton& getInstance() {
std::call_once(initFlag, [](){
instancePtr = new OnceSingleton();
});
return *instancePtr;
}
private:
OnceSingleton() {}
~OnceSingleton() {}
OnceSingleton(const OnceSingleton&) = delete;
OnceSingleton& operator=(const OnceSingleton&) = delete;
static OnceSingleton* instancePtr;
static std::once_flag initFlag;
};
OnceSingleton* OnceSingleton::instancePtr = nullptr;
std::once_flag OnceSingleton::initFlag;
优点
- 明确展示初始化逻辑,避免因局部静态对象的隐式实现而产生的疑惑。
std::call_once保证只调用一次,即使有异常也能保证安全。
缺点
- 需要手动管理实例指针,可能导致手动删除或内存泄漏。
- 相对
Meyers Singleton来说实现略显冗长。
4. 使用std::unique_ptr + std::atomic(现代化写法)
#include <memory>
#include <atomic>
#include <mutex>
class AtomSingleton {
public:
static AtomSingleton& instance() {
AtomSingleton* tmp = instancePtr.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instancePtr.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new AtomSingleton();
instancePtr.store(tmp, std::memory_order_release);
}
}
return *tmp;
}
private:
AtomSingleton() {}
~AtomSingleton() {}
AtomSingleton(const AtomSingleton&) = delete;
AtomSingleton& operator=(const AtomSingleton&) = delete;
static std::atomic<AtomSingleton*> instancePtr;
static std::mutex mtx;
};
std::atomic<AtomSingleton*> AtomSingleton::instancePtr{nullptr};
std::mutex AtomSingleton::mtx;
优点
- 使用
atomic和memory_order可显式控制可见性,适用于需要在多进程或低级别优化的场景。
缺点
- 代码复杂度较高,易出错;通常不需要这么底层的控制。
结论
- 对大多数应用,
Meyers Singleton(局部静态变量)是最推荐的实现方式。 - 如果你需要在构造失败时进行重试或需要更细粒度的异常处理,考虑
std::call_once。 - 只在特殊性能或兼容性需求下才使用双重检查锁或原子指针实现。
通过上述几种实现,你可以根据项目需求、编译器版本和线程安全要求,选择最合适的单例模式实现。