在多线程环境下,单例模式需要保证只有一个实例被创建,而且创建过程必须是线程安全的。下面以C++17为例,演示三种常用实现方式,并对比它们的优缺点。
1. 带锁的懒汉式
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
std::lock_guard<std::mutex> lock(mtx_);
if (!instance_) {
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 mtx_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mtx_;
优点
- 代码直观易懂
- 能在需要实例时才创建,符合懒加载需求
缺点
- 每次访问都要获取锁,导致性能瓶颈
instance_需要手动删除,容易出现内存泄漏
2. 双重检查锁(Double-Check Locking)
#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() = default;
~Singleton() = default;
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. 局部静态变量(C++11 线程安全初始化)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 起,局部静态对象的初始化是线程安全的
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 代码最简洁,完全不需要手动锁或原子操作
- 语言层面保证线程安全,符合标准
- 自动析构,避免内存泄漏
缺点
- 对于极端延迟要求的系统,第一次调用会有初始化开销
- 如果需要在程序退出时释放资源,需自行添加析构函数或使用
std::unique_ptr
4. 什么时候选择哪种实现?
| 场景 | 推荐实现 |
|---|---|
| 需要手动控制实例的生命周期,或在多模块中共享 | 带锁懒汉式(或双重检查锁) |
| 代码简洁优先,且不在意第一次访问的开销 | 局部静态变量 |
| 在非常高并发的读场景下,写入次数极少 | 双重检查锁(避免锁的频繁获取) |
5. 小结
- 线程安全 是实现单例模式的核心。
- 局部静态变量 在 C++11 之后提供了最简洁且安全的实现方式。
- 需要注意的是,单例模式虽然方便,但过度使用会导致代码耦合度高,单元测试困难。建议在业务场景中合理评估是否真的需要单例。