单例模式(Singleton Pattern)是一种常用的设计模式,用于保证一个类在整个程序生命周期中只存在一个实例,并提供一个全局访问点。随着多线程环境的普及,线程安全成为实现单例模式时必须解决的重要问题。以下内容将展示几种常见的线程安全单例实现方式,并讨论其优缺点,帮助你在项目中选择最合适的方法。
1. 经典的双重检查锁(Double-Checked Locking, DCL)
class Singleton {
public:
static Singleton& getInstance() {
if (instance_ == nullptr) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr) { // 第二次检查
instance_ = new Singleton();
}
}
return *instance_;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
- 优点:第一次访问时无锁开销,后续访问快速。
- 缺点:需要保证
new操作是可见的,C++11 引入了内存序保证;如果使用旧编译器或不正确的内存模型,可能出现 “使用未初始化的对象” 的情况。
2. 局部静态变量(Meyers Singleton)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 之后线程安全
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
- 优点:实现简单,编译器保证线程安全,适合大多数情况。
- 缺点:如果实例化时间不确定(如在程序退出时需要做清理),可能导致析构顺序问题;对懒加载有时不够精确。
3. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag_, []() {
instance_ = new Singleton();
});
return *instance_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
- 优点:显式表明一次性初始化,兼容多种编译器;避免了双重检查锁的复杂性。
- 缺点:需要手动释放资源(在程序退出时),否则可能产生泄漏。
4. 使用 std::shared_ptr 与 std::weak_ptr
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
std::lock_guard<std::mutex> lock(mutex_);
std::shared_ptr <Singleton> temp = instance_.lock();
if (!temp) {
temp = std::shared_ptr <Singleton>(new Singleton());
instance_ = temp;
}
return temp;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::weak_ptr <Singleton> instance_;
static std::mutex mutex_;
};
std::weak_ptr <Singleton> Singleton::instance_;
std::mutex Singleton::mutex_;
- 优点:能够在程序运行时动态销毁并重新创建实例,适用于需要重新初始化的场景。
- 缺点:使用
std::shared_ptr会有一定的性能开销,且需要注意线程安全的锁粒度。
5. 选型建议
| 场景 | 推荐实现 | 说明 |
|---|---|---|
| 最简洁、最常用 | 局部静态变量(Meyers) | 适合绝大多数单例需求 |
| 需要懒加载 | std::call_once 或 双重检查锁 |
明确控制初始化时机 |
| 可销毁/可重建 | std::weak_ptr + shared_ptr |
支持动态重建实例 |
| 旧编译器/跨平台 | std::call_once |
避免对编译器实现细节的依赖 |
6. 常见坑与注意事项
- 析构顺序:若单例使用局部静态变量,析构顺序在程序退出时不确定。若有依赖外部资源的析构,建议显式销毁或使用
std::unique_ptr与std::once_flag。 - 多线程递归调用:如果单例内部调用自己,会导致死锁(如果使用互斥锁)。此时应使用
std::call_once或Meyers。 - 跨库共享:不同动态库之间共享单例需要使用
__declspec(dllexport)/__declspec(dllimport),否则会产生多个实例。
小结
线程安全的单例实现不再是难题,C++11 及之后的标准为我们提供了多种简单、可靠的手段。根据项目需求(懒加载、可销毁、跨库共享等),选择合适的实现方式即可。记住:单例的真正价值在于简化全局状态管理,在使用时保持谨慎,避免过度依赖单例导致代码耦合过度。祝你编码愉快!