在多线程环境下,单例模式的实现需要保证只有一个实例被创建,并且在多线程访问时不出现竞争条件。C++11之后,标准库提供了原子操作和线程安全的静态局部变量初始化,使得实现线程安全的单例变得相对简单。以下将详细介绍几种常见实现方式,并比较它们的优缺点。
1. 基于C++11静态局部变量的懒加载单例
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 线程安全的懒加载
return instance;
}
// 禁止拷贝和移动构造
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
优点
- 代码最简洁。
- C++11标准保证了静态局部变量的初始化是线程安全的。
- 无需显式锁。
缺点
- 如果单例需要在程序退出前做清理,可能会导致析构顺序问题(如果依赖其他静态对象)。
- 不能在多线程程序中按需销毁单例。
2. 双重检查锁(Double-Checked Locking,DCL)
class Singleton {
public:
static Singleton* instance() {
Singleton* tmp = instance_;
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_;
if (!tmp) {
tmp = new Singleton();
instance_ = tmp;
}
}
return tmp;
}
// 需要手动销毁
static void destroy() {
std::lock_guard<std::mutex> lock(mutex_);
delete instance_;
instance_ = nullptr;
}
private:
Singleton() = default;
~Singleton() = default;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
优点
- 只在第一次初始化时加锁,后续访问不需要锁。
- 适用于需要在运行时销毁单例的场景。
缺点
- 代码稍显复杂。
- 需要注意内存可见性和指针原子性。
- 如果没有使用
std::atomic,会出现“脏读”问题。
3. Meyers单例 + std::call_once
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, [](){ instance_ = new Singleton(); });
return *instance_;
}
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
优点
std::call_once语义清晰,确保一次性初始化。- 可以与显式销毁配合使用。
缺点
- 与C++11静态局部变量相比,略显冗长。
- 仍需手动管理内存(如果想在程序结束前销毁)。
4. 线程安全的静态全局单例
如果单例不需要懒加载,而可以在程序启动时就创建,直接使用全局静态对象即可。
class Singleton {
public:
static Singleton& instance() {
return instance_;
}
private:
Singleton() = default;
~Singleton() = default;
static Singleton instance_;
};
Singleton Singleton::instance_;
优点
- 极其简单。
- 对象创建时间可控。
缺点
- 可能造成资源提前分配。
- 若单例依赖其他全局对象,初始化顺序成为隐式约束。
5. 使用 std::shared_ptr 管理生命周期
如果想让单例能够在多线程环境中被共享,并自动在最后一次使用后销毁,可以结合 std::shared_ptr:
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::call_once(flag_, [](){
ptr_ = std::shared_ptr <Singleton>(new Singleton());
});
return ptr_;
}
private:
Singleton() = default;
~Singleton() = default;
static std::shared_ptr <Singleton> ptr_;
static std::once_flag flag_;
};
std::shared_ptr <Singleton> Singleton::ptr_;
std::once_flag Singleton::flag_;
优点
- 线程安全。
- 自动内存管理,避免手动
delete。
缺点
- 需要额外的
std::shared_ptr代价(引用计数)。 - 仍然是懒加载。
6. 性能对比与选择建议
| 实现方式 | 线程安全 | 初始化方式 | 锁开销 | 可销毁 | 代码简洁度 |
|---|---|---|---|---|---|
| 静态局部 | ✔ | 懒加载 | 0 | 受限 | ★★★ |
| DCL | ✔ | 懒加载 | 仅第一次 | ✔ | ★★ |
| call_once | ✔ | 懒加载 | 仅第一次 | ✔ | ★★ |
| 静态全局 | ✔ | 预加载 | 0 | ✔ | ★★★ |
| shared_ptr+call_once | ✔ | 懒加载 | 仅第一次 | ✔ | ★★ |
- 如果只需要单例且不关心销毁顺序,首选 静态局部变量(Meyers单例)。
- 需要在运行时手动销毁,推荐 双重检查锁 或
std::call_once。 - 想让单例可在多线程间共享且自动销毁,使用
std::shared_ptr+call_once。 - 资源必须在程序启动前就可用,采用 静态全局单例。
7. 典型错误与陷阱
-
拷贝/移动构造/赋值
需要显式删除,否则其他线程可能会创建新的实例。 -
析构顺序问题
如果单例依赖其他全局对象,建议使用std::call_once并手动销毁,或者使用局部静态。 -
内存可见性
在没有std::atomic的双重检查锁实现中,可能出现“脏读”。务必使用std::atomic或std::call_once。 -
递归调用
单例初始化函数内部若再次调用instance(),可能导致死锁。避免在构造函数或析构函数内部调用instance()。
8. 结语
C++11及之后的标准为单例模式提供了多种线程安全实现方案。最常用且最简洁的方式是利用静态局部变量,得益于语言层面的线程安全保证。对于更细粒度的控制(如手动销毁、共享计数等),可以结合 std::call_once、std::once_flag 或 std::shared_ptr。在实际项目中,建议根据需求权衡性能、简洁度与生命周期管理,选取最合适的实现方式。