单例模式(Singleton Pattern)是一种常见的软件设计模式,用于确保一个类只有一个实例,并提供全局访问点。在多线程环境下实现线程安全的单例模式尤为重要,以避免竞争条件导致的错误实例化。下面我们从 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;
~Singleton() = default;
};
- 优点:实现最简洁,使用局部静态变量,编译器保证初始化是线程安全的。无需手动加锁,消除了锁带来的性能开销。
- 注意事项:在 C++11 之前的编译器中,局部静态变量的初始化并非线程安全。若在旧编译环境下,需要自行使用互斥锁。
2. 双重检查锁(Double-Checked Locking)
#include <mutex>
class Singleton {
public:
static Singleton* getInstance() {
if (!instance_) { // 第一层检查
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) { // 第二层检查
instance_ = new Singleton();
}
}
return instance_;
}
~Singleton() { delete instance_; }
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
- 优点:避免了每次访问都加锁,只有在首次创建实例时才需要加锁。
- 缺点:实现比较繁琐,易出错。由于编译器优化和 CPU 指令重排,早期的 C++ 实现需要使用
std::atomic或std::atomic_flag来确保可见性。
3. std::call_once 与 std::once_flag
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag_, [](){
instance_ = new Singleton();
});
return *instance_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
- 优点:
std::call_once通过std::once_flag内部实现了一次性初始化的原子性。代码简洁,兼容 C++11 及以后版本。 - 缺点:仍然使用裸指针,若想避免手动 delete,需要配合
std::unique_ptr。
4. 用 std::unique_ptr 结合 std::call_once
#include <mutex>
#include <memory>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag_, [](){
instance_ = std::unique_ptr <Singleton>(new Singleton());
});
return *instance_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
static std::unique_ptr <Singleton> instance_;
static std::once_flag initFlag_;
};
std::unique_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
- 优点:使用
unique_ptr自动管理内存,避免泄漏。线程安全初始化与访问。
5. 对比与选择
| 方法 | 实现简洁度 | 线程安全性 | 性能 | 兼容性 |
|---|---|---|---|---|
| Meyers 单例 | ★★★★★ | ★★★★★(C++11 以后) | ★★★★★ | C++11 以后 |
| 双重检查锁 | ★★ | ★★(取决实现) | ★★★ | 需要手动处理内存 |
call_once |
★★★ | ★★★★★ | ★★★★ | C++11 以后 |
call_once + unique_ptr |
★★★★ | ★★★★★ | ★★★★ | C++11 以后 |
在大多数现代 C++ 项目中,Meyers 单例 或 std::call_once 是推荐的做法。它们代码量少、易于维护,并且性能接近。
6. 常见陷阱
- 延迟析构:如果你使用裸指针或
std::unique_ptr,确保在程序结束前析构单例,或者让它驻留在进程全生命周期内。 - 跨线程静态对象初始化:即使使用
call_once,也要注意多线程环境下的静态对象销毁顺序问题。 - 递归调用:如果在单例构造函数内部再次访问
instance(),会导致死循环或未定义行为。
7. 小结
- 在 C++11 之后,使用局部静态变量(Meyers 单例)已足够满足大多数需求,且最简洁。
- 对于需要手动控制生命周期或更细粒度的控制,
std::call_once与std::once_flag提供了强大的工具。 - 双重检查锁在现代 C++ 中已不太推荐,除非你处于非常老的编译环境。
掌握这些实现方式后,你可以根据项目需求选择最合适的单例实现,确保线程安全与高性能并存。祝编码愉快!