在多线程环境下,保证单例对象只被实例化一次是一项挑战。下面我们从几种常见实现方式出发,演示如何在 C++ 中实现线程安全的单例模式,并讨论各自的优缺点。
1. 经典 Meyers 单例(C++11 起)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 线程安全的局部静态变量
return instance;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
-
优点
- 代码简洁,易于维护。
- 依赖编译器的
static局部变量初始化的线程安全保证(C++11 之后)。 - 延迟初始化:第一次调用
instance()时才构造对象。
-
缺点
- 如果单例需要在程序结束前做特殊清理,无法显式控制销毁顺序。
- 对于需要参数化构造的单例,无法直接使用。
2. std::call_once 与 std::once_flag
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, [](){ ptr.reset(new Singleton); });
return *ptr;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::unique_ptr <Singleton> ptr;
static std::once_flag initFlag;
};
std::unique_ptr <Singleton> Singleton::ptr = nullptr;
std::once_flag Singleton::initFlag;
-
优点
- 可以在单例构造函数中传递参数(通过 lambda 传递)。
- 对构造过程的控制更细,适合需要按需初始化的场景。
- 线程安全性明确显式,易于阅读。
-
缺点
- 代码略显繁琐。
std::unique_ptr需要额外的头文件支持。
3. 双重检查锁(Double-Check Locking)——注意 C++ 的实现细节
class Singleton {
public:
static Singleton* instance() {
Singleton* tmp = ptr;
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx);
tmp = ptr;
if (!tmp) {
ptr = tmp = new Singleton();
}
}
return tmp;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* ptr;
static std::mutex mtx;
};
Singleton* Singleton::ptr = nullptr;
std::mutex Singleton::mtx;
-
优点
- 第一次调用时无锁,后续调用无需加锁,性能更好。
-
缺点
- 需要在构造函数中保证对
ptr的写操作是可见的(使用std::atomic或std::memory_order)。 - 代码难以维护,易出错。
- 对于 C++11 之前的编译器,可能不安全。
- 需要在构造函数中保证对
4. 静态成员指针 + std::atomic(现代实现)
class Singleton {
public:
static Singleton* instance() {
Singleton* tmp = ptr.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx);
tmp = ptr.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton();
ptr.store(tmp, std::memory_order_release);
}
}
return tmp;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::atomic<Singleton*> ptr;
static std::mutex mtx;
};
std::atomic<Singleton*> Singleton::ptr{nullptr};
std::mutex Singleton::mtx;
-
优点
- 明确使用原子操作保证可见性,符合 C++11 的内存模型。
- 与双重检查锁类似,性能优越。
-
缺点
- 仍然需要
std::mutex作为同步手段,代码稍显繁琐。
- 仍然需要
5. 何时使用哪种实现?
| 场景 | 推荐实现 |
|---|---|
| 单例无参数、只读 | Meyers 单例 |
| 需要按需参数化构造 | std::call_once |
| 对性能要求极高,且对 C++11/17 兼容 | 双重检查 + std::atomic |
| 需要手动控制单例生命周期 | 静态成员 + std::call_once(结合智能指针) |
6. 小结
- 线程安全:C++11 之后,局部静态变量的初始化已经线程安全,推荐使用 Meyers 单例。
- 可配置性:若需要在单例构造时传参,
std::call_once能提供足够的灵活性。 - 性能:双重检查锁和原子指针结合可以在极端高并发场景中减少锁开销。
在实际项目中,往往选择最简单、最易维护的实现方式(Meyers 单例)即可满足大多数需求。只有在特殊情况下才需要更复杂的同步方案。