在现代 C++(C++11 及以后)中,线程安全的单例模式可以通过几种方式实现,其中最简洁、最可靠的方法是利用局部静态变量的“魔法”以及标准库的原语。下面详细说明几种实现方式,并对其优缺点进行比较。
1. 局部静态变量(Meyers 单例)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11保证线程安全的初始化
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() { /* 资源初始化 */ }
~Singleton() { /* 资源释放 */ }
};
优点
- 代码简洁,只需一行。
- 编译器自动保证初始化时的线程安全(C++11 标准)。
- 延迟初始化(懒加载),只有第一次调用
getInstance()时才构造对象。
缺点
- 不能控制实例析构的时机(仅在程序结束时析构)。
- 对于某些极端多线程场景(极高竞争)可能产生轻微的锁争用。
2. 双重检查锁(Double-Checked Locking) + std::atomic
#include <atomic>
#include <mutex>
class Singleton {
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mtx);
tmp = instance.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
// 其他成员...
private:
Singleton() {}
~Singleton() {}
static std::atomic<Singleton*> instance;
static std::mutex mtx;
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;
优点
- 只在第一次创建实例时加锁,后续访问无锁,性能高。
- 可在需要时手动销毁实例(通过
delete instance或std::unique_ptr包装)。
缺点
- 代码较为复杂,容易出错。
- 对 C++11 内存模型的正确使用要求较高。
3. std::call_once 与 std::once_flag
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(flag, []() { instance.reset(new Singleton()); });
return *instance;
}
private:
Singleton() {}
~Singleton() {}
static std::unique_ptr <Singleton> instance;
static std::once_flag flag;
};
std::unique_ptr <Singleton> Singleton::instance = nullptr;
std::once_flag Singleton::flag;
优点
- 代码比双重检查锁更简洁。
std::call_once内部实现已保证线程安全且无竞争开销。
缺点
- 与
std::atomic相比,仍存在一次锁操作,但几乎可以忽略。
4. 结合 std::shared_ptr 与 std::weak_ptr
如果单例需要在多处共享,并可能在程序运行期间被销毁后重新创建,可以考虑:
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
std::lock_guard<std::mutex> lock(mtx);
if (auto ptr = instance.lock()) {
return ptr;
}
auto newInstance = std::make_shared <Singleton>();
instance = newInstance;
return newInstance;
}
private:
Singleton() {}
static std::weak_ptr <Singleton> instance;
static std::mutex mtx;
};
std::weak_ptr <Singleton> Singleton::instance;
std::mutex Singleton::mtx;
优点
- 可在多线程间安全共享实例。
- 支持在实例被销毁后自动重建。
缺点
- 引入了引用计数,略微影响性能。
- 需要手动管理锁。
5. 对比与最佳实践
| 实现方式 | 线程安全保障 | 性能 | 可定制性 | 代码复杂度 |
|---|---|---|---|---|
| 局部静态 | 自动(C++11) | 高(无锁) | 低(仅在程序退出析构) | 低 |
| 双重检查锁 | 手动(原子 + mutex) | 很高 | 高(可手动销毁) | 高 |
| call_once | 自动(once_flag) | 高 | 中 | 中 |
| weak_ptr + mutex | 手动 | 中 | 高(可复用) | 高 |
- 如果你只需要一个简单的、全局生命周期的单例,推荐使用 局部静态变量(Meyers 单例)。它是最安全、最简单且符合现代 C++ 标准的实现。
- 如果需要手动销毁或在多次调用间重建实例,建议使用
std::call_once+std::unique_ptr或 双重检查锁(后者更适合极端性能要求场景)。 - 如果单例需要在不同线程间共享且可能被销毁后重建,使用
std::shared_ptr+std::weak_ptr方案。
6. 常见陷阱
- C++03 版本:局部静态变量的初始化不是线程安全的,必须手动加锁或使用
std::mutex。 - 多线程递归:如果单例在构造过程中再次调用
getInstance(),要确保使用std::call_once或局部静态,否则可能导致死锁或未定义行为。 - 静态变量初始化顺序:跨文件的静态单例在不同翻译单元中可能产生“静态初始化顺序问题”。使用局部静态可以规避此问题。
- 内存泄漏:使用
new的单例需要手动delete或使用智能指针避免泄漏。
7. 结语
C++11 以后,单例模式的实现已大大简化。最安全、最简洁的方案是局部静态变量,但在需要更细粒度控制时,std::call_once 或双重检查锁仍然是不错的选择。掌握好线程安全和资源管理的细节,能让你在并发程序中放心使用单例而不必担心竞争条件。