在多线程环境下,单例模式的实现需要特别注意线程安全性。下面介绍几种常用的实现方式,并讨论它们的优缺点。
1. 经典Meyers Singleton(局部静态变量)
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 起,局部静态变量的初始化已保证原子性,避免了双重检查锁的复杂性。
缺点
- 不可在类析构前显式销毁:若需要在程序结束前手动销毁对象,可使用
std::unique_ptr配合std::atexit实现。 - 调试困难:如果构造函数抛异常,可能导致后续调用失败。
2. 双重检查锁(DCL)
class Singleton {
public:
static Singleton* instance() {
if (!instance_) {
std::lock_guard<std::mutex> lock(mtx_);
if (!instance_) {
instance_ = new Singleton();
}
}
return instance_;
}
static void destroy() {
std::lock_guard<std::mutex> lock(mtx_);
delete instance_;
instance_ = nullptr;
}
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::mutex mtx_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mtx_;
优点
- 适用于 C++11 前的编译器。
- 能在程序结束前手动销毁单例。
缺点
- 实现复杂:需要正确使用
volatile(或std::atomic)和双重检查。 - 潜在的优化缺陷:编译器可能重排指令,导致可见性问题。
- 性能:第一次访问时会进行两次检查和一次锁操作,略低于Meyers实现。
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确保初始化只执行一次。 - 清晰易懂:不需要手动加锁。
- 兼容性好:适用于所有 C++11 及之后的标准。
缺点
- 仍需手动销毁(如果想释放资源)。
- 与Meyers Singleton相比,代码略显冗长。
4. 静态函数对象(Lambda)
class Singleton {
public:
static Singleton& instance() {
static auto ptr = []() -> Singleton* {
return new Singleton();
}();
return *ptr;
}
private:
Singleton() = default;
};
优点
- 利用Lambda延迟实例化,兼顾线程安全。
- 可通过返回指针实现懒销毁。
缺点
- 仍为C++11实现,代码略显复杂。
5. 线程安全的懒加载+智能指针
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::call_once(flag_, [](){ ptr_ = std::make_shared <Singleton>(); });
return ptr_;
}
private:
Singleton() = default;
static std::shared_ptr <Singleton> ptr_;
static std::once_flag flag_;
};
std::shared_ptr <Singleton> Singleton::ptr_ = nullptr;
std::once_flag Singleton::flag_;
优点
- 自动管理生命周期,避免手动delete。
- 可多线程共享同一实例。
缺点
shared_ptr会引入一次引用计数的开销。
何时选哪种实现?
| 场景 | 推荐实现 | 理由 |
|---|---|---|
| 只需要单例,且C++11+ | Meyers Singleton | 简洁、线程安全、延迟 |
| 需要手动销毁或在C++11之前编译 | 双重检查锁 | 兼容性 |
| 需要显式一次性初始化控制 | std::call_once |
语义清晰、线程安全 |
| 需要共享计数、可能在多个线程释放 | shared_ptr + call_once |
自动内存管理 |
常见陷阱
-
静态对象销毁顺序
- 如果多个单例相互依赖,可能出现“static deinitialization order fiasco”。
- 解决方案:使用
std::call_once或Meyers Singleton,避免在析构中访问其他静态对象。
-
抛异常的构造函数
- Meyers Singleton 在构造抛异常后,后续再次调用会再次尝试初始化,可能导致重复异常。
- 可使用
std::unique_ptr包装并在异常时清理。
-
多进程环境
- 单例只能在进程内唯一。若跨进程需要使用共享内存或文件锁。
小结
C++11以后,Meyers Singleton 与 std::call_once 已经可以轻松实现线程安全的单例,开发者可以根据项目需求选择最适合的实现方式。关键是保证延迟初始化、一次性执行以及线程安全,并注意对象销毁时机和依赖关系。祝编码愉快!