在多线程环境下,保证单例实例的线程安全是设计模式中的一个重要挑战。下面介绍几种在C++中实现线程安全单例的常用方法,并讨论它们的优缺点。
1. Meyers 单例(C++11 之后的线程安全局部静态变量)
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 的线程安全局部静态变量语义。
- 缺点:如果你需要在销毁时执行特定逻辑,C++11 标准并不保证析构顺序;在某些嵌入式环境下,可能不支持 C++11。
2. 双重检查锁(Double-Checked Locking)
class Singleton {
public:
static Singleton* getInstance() {
if (!instance_) {
std::lock_guard<std::mutex> lock(mtx_);
if (!instance_) {
instance_ = new Singleton();
}
}
return instance_;
}
private:
Singleton() = default;
static Singleton* instance_;
static std::mutex mtx_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mtx_;
- 优点:在第一次创建实例后,后续访问不需要锁,性能相对较好。
- 缺点:实现细节比较繁琐;在 C++11 之前的编译器中,可能由于内存可见性问题导致错误;需要使用
std::atomic或volatile来保证可见性。
3. 静态局部变量加 std::call_once
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag_, []{
instance_ = new Singleton();
});
return *instance_;
}
private:
Singleton() = default;
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
- 优点:
std::call_once由标准库实现,线程安全且易于理解。 - 缺点:同样需要手动销毁(如通过
std::unique_ptr或在atexit注册),如果不销毁会导致资源泄漏。
4. 用 std::shared_ptr 与 std::make_shared
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
static std::shared_ptr <Singleton> instance = std::make_shared<Singleton>();
return instance;
}
private:
Singleton() = default;
};
- 优点:使用
std::shared_ptr自动管理生命周期,线程安全性与局部静态变量相同。 - 缺点:在极端情况下,
std::shared_ptr的引用计数操作可能会产生额外开销。
5. 枚举实现(Java 风格,C++ 并不适用)
C++ 中不支持使用枚举实现单例,因其枚举不能包含成员函数。
何时选择哪种实现?
| 场景 | 推荐实现 |
|---|---|
| 只需要单例,不需要自定义析构 | Meyers 单例(局部静态) |
| 需要在销毁时执行特定逻辑 | std::call_once 或 std::unique_ptr |
| 需要在旧编译器下兼容 | 双重检查锁 + std::atomic |
| 需要对实例进行计数或共享 | std::shared_ptr |
小结
C++11 之后,最推荐使用的是 Meyers 单例(局部静态变量),因为它实现最简单、最可靠,且符合标准库的线程安全保证。若对销毁顺序有严格要求,可考虑 std::call_once 或 std::unique_ptr。在旧环境或特殊需求下,双重检查锁和 std::shared_ptr 仍是可行的替代方案。