在多线程环境下,单例模式(Singleton)经常会成为资源共享的瓶颈。传统的单例实现虽然保证了全局唯一性,但在并发访问时可能会出现竞争条件,导致多次实例化或者线程不安全。下面我们从几个角度来探讨如何在C++中实现线程安全的单例模式,并给出几种常见的实现方式。
1. 经典 Meyers 单例(C++11 起)
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;
};
原理
- 局部静态变量:在第一次进入
instance()时,编译器会保证instance的构造是原子性的。自 C++11 起,标准保证多线程下局部静态变量的初始化是线程安全的。 - 删除拷贝构造和赋值:防止通过拷贝或赋值创建新的实例。
优点
- 代码简洁,易于维护。
- 无需手动加锁或使用原子操作。
- 延迟初始化:真正需要时才实例化。
缺点
- 对于极低延迟要求或在构造过程中有异常抛出的情况,需要额外处理。
- 对于单元测试,难以重置实例。
2. 双重检查锁(Double-Checked Locking)
class Singleton {
public:
static Singleton* instance() {
if (instance_ == nullptr) { // 第一检查
std::lock_guard<std::mutex> lock(mutex_);
if (instance_ == nullptr) { // 第二检查
instance_ = new Singleton();
}
}
return instance_;
}
// 其它成员...
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保证可见性。
优点
- 只在第一次访问时进行加锁,后续访问不受锁影响。
缺点
- 需要手动维护
instance_与mutex_。 - 如果构造函数抛出异常,
instance_可能保持为nullptr,导致后续访问仍然进入临界区。
3. 静态局部 + std::call_once
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, []() {
instance_ = new Singleton();
});
return *instance_;
}
// 其它成员...
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
原理
std::call_once保证闭包只会执行一次,线程安全。- 与
std::atomic或手动加锁相比,更简洁。
优点
- 简洁、易读,适合需要手动延迟初始化的场景。
- 适用于 C++11 之前的编译器,只要支持
std::call_once。
4. 延迟单例(Lazy Singleton)与智能指针
如果你需要在单例销毁时释放资源,可以使用 std::shared_ptr 与 std::weak_ptr:
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::lock_guard<std::mutex> lock(mutex_);
if (!ptr_) {
ptr_ = std::shared_ptr <Singleton>(new Singleton());
}
return ptr_;
}
private:
Singleton() = default;
static std::shared_ptr <Singleton> ptr_;
static std::mutex mutex_;
};
std::shared_ptr <Singleton> Singleton::ptr_{nullptr};
std::mutex Singleton::mutex_;
说明
- 通过
std::shared_ptr自动管理生命周期,支持多次获取实例。 - 线程安全地创建与销毁。
5. 性能考虑
- Meyers 单例 在现代编译器下是最快的,因为只在第一次调用时加锁,且编译器会优化为局部静态构造。
- 双重检查锁 可能因
std::atomic的可见性开销略慢。 std::call_once具有较好的性能,尤其在大多数调用不需要锁时。
6. 实践中的常见误区
-
在构造函数中访问全局单例
这会导致构造函数执行时单例未完全初始化,产生未定义行为。建议将初始化放在instance()之外。 -
使用宏定义实现单例
宏会隐藏错误,难以调试。推荐使用类封装。 -
不处理异常
单例构造抛异常后,后续调用可能再次尝试创建实例。使用std::call_once或try/catch进行保护。
7. 结语
在 C++11 之后,实现线程安全的单例几乎不再是难题。最推荐的做法是使用局部静态变量(Meyers 单例),因为其既简洁又符合标准。对于更复杂的需求,如手动销毁或多线程初始化控制,可以考虑 std::call_once 或双重检查锁方案。只要注意构造函数的异常安全性和生命周期管理,单例模式就能在多线程环境下保持稳定与高效。