在多线程环境下,单例模式常用于控制全局资源的唯一实例。传统的实现方式(如懒汉式、饿汉式)在 C++11 之后可以利用语言级别的线程安全特性简化实现。下面介绍几种常见的线程安全单例实现,并比较它们的优缺点。
1. 局部静态变量(Meyers 单例)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 规定此初始化是线程安全的
return instance;
}
// 其他接口
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 代码简洁,几乎不需要额外的同步机制。
- 只在第一次调用时创建实例,后续调用成本极低。
- 适用于大多数情况,推荐使用。
缺点
- 仅适用于 C++11 及以上。
- 对于需要在程序关闭前手动销毁资源的场景(例如动态链接库卸载)不太友好,实例会在程序退出时自动析构。
2. 双重检查锁(Double-Check Locking,DCL)
class Singleton {
public:
static Singleton* instance() {
if (!ptr_) {
std::lock_guard<std::mutex> lock(mtx_);
if (!ptr_) {
ptr_ = new Singleton();
}
}
return ptr_;
}
private:
Singleton() = default;
static Singleton* ptr_;
static std::mutex mtx_;
};
Singleton* Singleton::ptr_ = nullptr;
std::mutex Singleton::mtx_;
优点
- 对资源的访问比局部静态变量更灵活,可以自行控制对象的生命周期。
- 在某些场景下(如需要在某一阶段销毁实例)更适用。
缺点
- 代码稍显复杂,需要显式的锁和双重检查。
- 需要注意指针的原子性,避免在多线程下出现指针不一致的情况(C++11 的
std::atomic可以帮助解决)。
3. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, []{ instance_ = new Singleton(); });
return *instance_;
}
private:
Singleton() = default;
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
优点
std::call_once的语义非常明确,适用于一次性初始化。- 线程安全且性能良好,锁的持有时间极短。
缺点
- 仍需要手动删除实例,若不手动删除会造成资源泄漏。
4. 智能指针 + std::shared_ptr
如果想在多线程环境中共享单例,同时在不再使用时自动销毁,可以结合 std::shared_ptr 与 std::call_once:
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::call_once(flag_, []{
instance_ = std::shared_ptr <Singleton>(new Singleton());
});
return instance_;
}
private:
Singleton() = default;
static std::shared_ptr <Singleton> instance_;
static std::once_flag flag_;
};
std::shared_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
此时单例对象的生命周期由 std::shared_ptr 管理,使用完后会自动析构。
5. 何时使用哪种实现?
| 场景 | 推荐实现 |
|---|---|
| 简单且不需要手动销毁 | Meyers 单例 |
| 需要手动销毁资源(如动态库) | std::call_once + std::shared_ptr |
| 需要自定义销毁时机 | std::call_once + raw pointer |
| 对于 C++11 之前的代码 | 双重检查锁(但需谨慎) |
6. 常见错误
-
忘记删除拷贝构造/赋值操作
Singleton必须把拷贝构造函数和赋值操作符删除或设为delete,否则可能产生多实例。 -
多线程未同步的写
在双重检查锁或std::call_once之外,任何对实例的写操作都需要加锁。 -
静态变量在多线程环境下的销毁
Meyers 单例的实例在程序退出时会被销毁,若析构时依赖其他静态对象,可能导致顺序问题。可采用std::atexit注册自定义析构或使用智能指针。
7. 小结
在 C++11 及以后,最简洁且安全的单例实现就是局部静态变量(Meyers 单例)。如果需要更细粒度的控制生命周期或在多线程环境中动态销毁实例,std::call_once 与 std::shared_ptr 是更好的选择。始终记住,单例模式虽然方便,但也要慎用,避免过度使用导致的耦合和难以测试的问题。