在 C++ 中,单例模式(Singleton Pattern)用于确保某个类只有一个实例,并提供全局访问点。随着多线程编程的普及,传统的单例实现往往在并发环境下会产生竞争条件,导致多实例或初始化失败。本文将介绍几种线程安全的单例实现方式,并讨论其优缺点。
1. 经典懒汉式(双检锁 + std::atomic)
class LazySingleton {
public:
static LazySingleton& getInstance() {
LazySingleton* 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 LazySingleton();
instance.store(tmp, std::memory_order_release);
}
}
return *tmp;
}
private:
LazySingleton() = default;
~LazySingleton() = default;
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
static std::atomic<LazySingleton*> instance;
static std::mutex mtx;
};
std::atomic<LazySingleton*> LazySingleton::instance{nullptr};
std::mutex LazySingleton::mtx;
- 优点:延迟实例化,第一次访问才创建对象。
- 缺点:实现相对复杂,双检锁模式在某些编译器/硬件上可能失效,需要使用
std::atomic或std::mutex。
2. 局部静态变量(Meyer’s Singleton)
class MeyersSingleton {
public:
static MeyersSingleton& getInstance() {
static MeyersSingleton instance; // C++11 保证线程安全
return instance;
}
private:
MeyersSingleton() = default;
~MeyersSingleton() = default;
MeyersSingleton(const MeyersSingleton&) = delete;
MeyersSingleton& operator=(const MeyersSingleton&) = delete;
};
- 优点:代码简洁,编译器保证线程安全。
- 缺点:无法在程序结束前显式销毁实例,可能导致资源泄漏(尤其是与单例内部文件句柄、网络连接等非托管资源相关)。
3. 递归锁与静态局部(实现细节)
如果需要在单例内部使用互斥锁,建议使用 std::call_once,可以避免递归锁带来的问题:
class OnceSingleton {
public:
static OnceSingleton& getInstance() {
std::call_once(initFlag, []() { instance = new OnceSingleton(); });
return *instance;
}
private:
OnceSingleton() = default;
~OnceSingleton() = default;
OnceSingleton(const OnceSingleton&) = delete;
OnceSingleton& operator=(const OnceSingleton&) = delete;
static OnceSingleton* instance;
static std::once_flag initFlag;
};
OnceSingleton* OnceSingleton::instance = nullptr;
std::once_flag OnceSingleton::initFlag;
- 优点:
std::call_once保证一次初始化,无需手动管理锁。 - 缺点:同样无法提前销毁实例。
4. 对齐与内存管理(C++17 并行化)
C++17 引入了 std::shared_mutex,可以在读取时共享锁,而写入时独占锁。对于单例而言,一般只需要在初始化时加锁,后续无需再锁。示例:
class AlignSingleton {
public:
static AlignSingleton& getInstance() {
std::shared_lock<std::shared_mutex> readLock(mtx);
if (!instance) {
readLock.unlock(); // 释放共享锁
std::unique_lock<std::shared_mutex> writeLock(mtx);
if (!instance) { // 双检锁
instance = new AlignSingleton();
}
}
return *instance;
}
private:
AlignSingleton() = default;
~AlignSingleton() = default;
AlignSingleton(const AlignSingleton&) = delete;
AlignSingleton& operator=(const AlignSingleton&) = delete;
static AlignSingleton* instance;
static std::shared_mutex mtx;
};
AlignSingleton* AlignSingleton::instance = nullptr;
std::shared_mutex AlignSingleton::mtx;
- 优点:在高并发读取场景下,减少锁竞争。
- 缺点:实现更复杂,适用于特殊需求。
5. 小结与最佳实践
| 实现方式 | 适用场景 | 代码复杂度 | 销毁控制 |
|---|---|---|---|
| 双检锁 + atomic | 延迟初始化且需要手动销毁 | 中等 | 手动 delete 或 std::unique_ptr |
| Meyer’s Singleton | 简洁,资源释放无关 | 低 | 由系统析构 |
| std::call_once | 线程安全且一次初始化 | 低 | 由系统析构 |
| shared_mutex | 高并发读 | 高 | 由系统析构 |
推荐:对于大多数项目,Meyer’s Singleton 或 std::call_once 是最安全、最简洁的选择。若需要在单例内部管理非托管资源,建议在单例类中使用 std::unique_ptr 或自定义销毁方法,并在程序结束前显式调用。
6. 进一步阅读
- 《C++ Concurrency in Action》——讨论线程安全的实践
- ISO C++ 标准中
thread、mutex、atomic章节 - 相关开源实现(如 Google Guava 的
Singleton、Boost 的singleton模块)
通过上述几种实现方式,你可以根据项目需求在 C++ 中安全、高效地使用单例模式。