在 C++11 之后,语言标准引入了对多线程原语的直接支持,例如 std::mutex、std::lock_guard、以及原子操作等。借助这些工具,我们可以很方便地实现一个线程安全的单例(Singleton)模式。以下内容将从理论到实践,逐步展示如何构建并使用一个线程安全的单例。
1. 单例模式回顾
单例模式保证一个类只有一个实例,并提供全局访问点。传统实现方式通常采用懒加载(lazy initialization)与双重检查锁(double-checked locking)或使用 static 局部变量(Meyer’s singleton)。
在单线程环境下,这些实现都足够,但当多线程同时访问单例初始化时,可能会出现竞争条件,导致实例被多次创建或访问到未完成构造的对象。
2. C++11 的 static 局部变量
C++11 规范保证了局部静态变量在多线程环境下的初始化是线程安全的。最简洁的单例实现如下:
class ThreadSafeSingleton {
public:
static ThreadSafeSingleton& getInstance() {
static ThreadSafeSingleton instance; // C++11 保证线程安全初始化
return instance;
}
// 其它业务方法
void doSomething() { /* ... */ }
private:
ThreadSafeSingleton() = default;
~ThreadSafeSingleton() = default;
ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
};
优点
- 代码简洁,易于维护。
- 运行时开销几乎为零,只有一次锁的内部检查。
缺点
- 如果实例需要提前销毁(如在
atexit前),需要显式手动销毁。 - 在极端情况下,初始化时出现异常会导致以后无法再次获取实例。
3. 经典双重检查锁实现(C++11 兼容)
如果你想更显式地掌控锁的行为,可以使用 std::call_once 或手动实现双重检查锁。下面给出 std::call_once 的实现:
class ThreadSafeSingleton {
public:
static ThreadSafeSingleton& getInstance() {
std::call_once(initFlag, []() {
instance.reset(new ThreadSafeSingleton);
});
return *instance;
}
void doSomething() { /* ... */ }
private:
ThreadSafeSingleton() = default;
~ThreadSafeSingleton() = default;
ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
static std::unique_ptr <ThreadSafeSingleton> instance;
static std::once_flag initFlag;
};
// 静态成员定义
std::unique_ptr <ThreadSafeSingleton> ThreadSafeSingleton::instance;
std::once_flag ThreadSafeSingleton::initFlag;
说明
std::call_once确保 lambda 只执行一次,且线程安全。std::once_flag为一次性标志。std::unique_ptr用于管理实例的生命周期,避免手动delete。
4. 原子指针 + Compare-And-Swap(CAS)实现
如果你希望完全手动控制,或者在不想使用 std::call_once 的旧编译器下实现,可以使用原子指针和 CAS 操作:
#include <atomic>
class ThreadSafeSingleton {
public:
static ThreadSafeSingleton& getInstance() {
ThreadSafeSingleton* tmp = instance.load(std::memory_order_acquire);
if (!tmp) {
ThreadSafeSingleton* newInstance = new ThreadSafeSingleton;
if (!instance.compare_exchange_strong(tmp, newInstance,
std::memory_order_release,
std::memory_order_acquire)) {
delete newInstance; // 已有人创建,回收新建实例
} else {
tmp = newInstance;
}
}
return *tmp;
}
void doSomething() { /* ... */ }
private:
ThreadSafeSingleton() = default;
~ThreadSafeSingleton() = default;
ThreadSafeSingleton(const ThreadSafeSingleton&) = delete;
ThreadSafeSingleton& operator=(const ThreadSafeSingleton&) = delete;
static std::atomic<ThreadSafeSingleton*> instance;
};
// 静态成员定义
std::atomic<ThreadSafeSingleton*> ThreadSafeSingleton::instance{nullptr};
优点
- 没有锁的开销。
- 适合极高频的单例访问场景。
缺点
- 代码复杂,易出现错误。
- 需要手动管理内存,易产生泄漏。
5. 如何选择?
| 方法 | 代码复杂度 | 运行时开销 | 兼容性 | 典型使用场景 |
|---|---|---|---|---|
| Meyer’s singleton | 简单 | 低 | C++11+ | 通用场景 |
std::call_once |
中等 | 低 | C++11+ | 需要显式控制 |
| 原子 + CAS | 高 | 低 | C++11+ | 高并发、性能敏感 |
在大多数项目中,Meyer’s singleton 就足够使用;如果你需要更细粒度的控制或者想让编译器知道你在考虑多线程,使用 std::call_once 是更安全的选择。
6. 线程安全的单例常见陷阱
-
对象的懒初始化与销毁时机冲突
- 线程正在访问实例时,主线程可能在
atexit期间销毁它,导致悬空指针。 - 解决方案:让单例在整个程序生命周期内存在,或使用
std::shared_ptr与std::weak_ptr的组合。
- 线程正在访问实例时,主线程可能在
-
拷贝构造/赋值被遗漏
- 必须显式
delete拷贝构造函数和赋值运算符,否则会出现多实例。
- 必须显式
-
异常安全
- 如果构造函数抛异常,保证不留下半构造对象。
std::call_once与原子实现天然异常安全;Meyer’s singleton 需要在try-catch中包装。
-
多进程环境
- 单例只在进程内保证唯一性;在多进程共享内存等场景下,需要额外同步。
7. 结语
C++11 之后,单例模式的线程安全实现变得异常简单。开发者可以根据项目需求、性能要求以及代码维护的便利性,在三种主流实现方式中进行选择。只要遵循基本原则(禁止拷贝、保证一次性初始化、异常安全),就能得到一个既可靠又高效的全局对象。