在 C++ 程序中,单例模式(Singleton)是一种常用的设计模式,用于确保某个类只有一个实例,并提供一个全局访问点。传统的单例实现方法往往在多线程环境下出现竞态条件,导致可能生成多个实例。下面通过几种方式,演示在 C++11 及以上标准中实现线程安全单例的方法,并讨论各自的优缺点。
1. Meyers 单例(局部静态变量)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后保证线程安全
return instance;
}
// 删除拷贝构造和赋值操作符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
原理:C++11 标准规定,局部静态变量的初始化是线程安全的。首次调用 instance() 时,instance 对象会被构造;随后任何线程访问都会获得同一实例。
优点
- 简洁、易于使用。
- 只在第一次使用时初始化,省去全局初始化的复杂性。
- 编译器自动处理多线程同步,代码可读性高。
缺点
- 对象销毁顺序不确定,可能导致在程序退出时出现悬挂引用。
- 无法在对象构造失败时抛出异常或返回错误码。
- 需要在 C++11 及以上编译器支持。
2. 经典 double‑checked locking
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() { delete instance_; }
private:
Singleton() = default;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
原理:使用 std::mutex 对首次实例化进行同步。第一次检查尝试避免无谓的锁竞争,第二次检查确保线程安全。
优点
- 延迟初始化,适用于在某些特定时刻才需要实例的情况。
- 兼容 C++11 以下版本(需要手动添加
std::mutex)。
缺点
- 需要手动处理单例销毁,容易出现内存泄漏。
- 代码相对繁琐,易出错。
- 由于
instance_是裸指针,若对象构造抛异常会导致未定义行为。
3. 函数静态变量与 std::call_once
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(flag_, []{
instance_ = new Singleton();
});
return *instance_;
}
// 必须手动销毁实例
static void destroy() {
delete instance_;
instance_ = nullptr;
}
private:
Singleton() = default;
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
原理:std::call_once 保证回调函数只被调用一次,内部使用轻量级互斥锁实现线程安全。
优点
- 与 double‑checked locking 相比,代码更简洁。
std::once_flag的实现更高效,避免无谓的锁竞争。- 兼容 C++11 及以上。
缺点
- 需要手动销毁实例,程序退出时仍需保证调用
destroy()。 - 与
Meyers单例相比,初始化更显式。
4. 对象池式实现(使用 std::shared_ptr)
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
static std::shared_ptr <Singleton> instance_ptr(new Singleton, [](Singleton*){/* cleanup */});
return instance_ptr;
}
private:
Singleton() = default;
};
原理:使用 std::shared_ptr 代替裸指针,借助局部静态变量实现线程安全。析构函数会在程序结束时自动调用。
优点
- 自动管理生命周期,避免手动销毁。
- 对象可以在不同模块之间共享引用计数。
- 简单易用。
缺点
- 引入
shared_ptr的开销(引用计数)。 - 在某些嵌入式或高性能场景下可能不适用。
5. 线程安全的懒加载(懒汉式)结合 std::atomic
class Singleton {
public:
static Singleton* getInstance() {
Singleton* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton();
instance_.store(tmp, std::memory_order_release);
}
}
return tmp;
}
private:
Singleton() = default;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
原理:通过 std::atomic 与轻量级互斥锁相结合,避免在实例已存在时产生锁竞争。
优点
- 兼容 C++11 及以上。
- 延迟初始化 + 线程安全,适合性能敏感场景。
缺点
- 代码相对复杂,难以维护。
- 仍需手动销毁实例。
何时选择哪种实现?
| 实现方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Meyers 单例 | 小型项目、快速原型 | 简洁、自动线程安全 | 销毁顺序不确定 |
| double‑checked | 需要兼容 C++03、复杂销毁流程 | 延迟加载 | 代码繁琐、易出错 |
call_once |
需要显式销毁、性能敏感 | 轻量级、简洁 | 仍需手动销毁 |
shared_ptr |
对象需要在多处共享 | 自动生命周期管理 | 计数开销 |
| atomic + mutex | 极端性能要求 | 细粒度控制 | 复杂、手动销毁 |
结语
在现代 C++ 开发中,推荐使用 Meyers 单例(局部静态变量)或 std::call_once 的实现。它们既满足线程安全,又保持代码简洁。若项目对单例的销毁顺序有严格要求,或需要在 C++03 环境下实现,可考虑 double‑checked locking 或 atomic + mutex 的方案。合理选择实现方式,能够让你在编写高并发 C++ 程序时,既保持代码质量,又避免潜在的并发错误。