实现线程安全的单例模式是很多项目中的常见需求,尤其是在多线程环境下。下面将详细介绍几种实现方式,并说明它们的优缺点以及适用场景。
1. 经典 Meyers 单例(C++11 之后)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后的局部静态变量初始化是线程安全的
return instance;
}
// 其他公开成员
void doSomething() { /*...*/ }
private:
Singleton() {} // 私有构造函数
~Singleton() {} // 私有析构函数
Singleton(const Singleton&) = delete; // 禁止拷贝构造
Singleton& operator=(const Singleton&) = delete; // 禁止赋值
};
优点
- 简洁:几行代码即可完成。
- 延迟初始化:实例在第一次访问时创建。
- 线程安全:C++11 标准保证局部静态变量的初始化是线程安全的。
缺点
- 不可控:无法在单例实例化之前执行其他初始化代码。
- 销毁时机:在程序退出时销毁顺序不确定,可能导致析构时依赖其他资源已被销毁。
2. 双重检查锁(DCLP)+ 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() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::atomic<Singleton*> instance;
static std::mutex mtx;
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::mutex Singleton::mtx;
优点
- 延迟初始化:实例仅在需要时创建。
- 可控销毁:可以手动删除实例,避免析构顺序问题。
缺点
- 复杂度高:需要手动管理原子指针和互斥锁。
- 潜在错误:若使用不当,可能导致竞态或内存泄漏。
3. 使用 std::call_once
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, []{
instancePtr = new Singleton();
});
return *instancePtr;
}
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instancePtr;
static std::once_flag initFlag;
};
Singleton* Singleton::instancePtr = nullptr;
std::once_flag Singleton::initFlag;
优点
- 简洁可靠:
std::call_once保证初始化只执行一次,且线程安全。 - 可控销毁:同样可以自行管理实例的生命周期。
缺点
- 与双重检查锁相似,仍需手动管理析构顺序。
4. 依赖于第三方库(如 Boost)
Boost 的 boost::singleton 提供了更完整的单例实现,并且可以自定义销毁顺序。但在大多数现代项目中,标准库已足够使用。
5. 何时选择哪种实现?
| 场景 | 推荐实现 |
|---|---|
| 简单、无需额外初始化 | Meyers 单例 |
| 需要在单例构造前执行其他操作或手动销毁 | std::call_once 或 DCLP |
| 对初始化顺序极为敏感 | 结合 std::atexit 或第三方库 |
6. 常见陷阱与注意事项
- 懒加载与销毁:如果单例持有全局资源,需确保在销毁时资源仍可用。常用方法是使用
std::shared_ptr或在main结束前手动delete。 - 多线程测试:即使编译器声明是线程安全,也建议在实际多线程环境中进行测试,尤其在旧编译器或嵌入式平台。
- 递归初始化:如果单例的构造函数间接访问同一个单例,可能导致未定义行为。可通过
std::call_once的内部实现避免。
7. 小结
在 C++11 之后,最推荐的单例实现是 Meyers 单例,因为其既简洁又安全。若项目有特殊需求(如手动销毁、依赖其它全局对象等),可以考虑 std::call_once 或双重检查锁实现。无论选择哪种方式,都应充分理解其线程安全机制,并结合项目实际情况做出决定。
祝你编码愉快!