在多线程环境下,单例模式的实现必须保证线程安全,否则可能导致多个实例被创建、资源竞争甚至崩溃。下面我们详细介绍几种常见的实现方式,并给出对应的代码示例,帮助你在实际项目中选择最合适的方案。
1. 基础知识回顾
- 单例模式(Singleton):确保一个类只有一个实例,并提供全局访问点。
- 线程安全:多线程访问同一资源时,必须保证不会出现竞态条件。
- 懒加载(Lazy Initialization):只有在第一次访问时才创建实例。
2. 实现方式对比
| 方案 | 关键点 | 代码复杂度 | 适用场景 |
|---|---|---|---|
① C++11 之 std::call_once + std::once_flag |
只在第一次调用时初始化一次,随后直接返回实例 | 简单、易读 | 适合所有标准 C++11 及以上版本 |
| ② 双重检查锁(Double-Check Locking) | 通过局部锁和 volatile/std::atomic 判断 |
需要注意内存模型 | 兼容老版本(C++03) |
| ③ 静态局部变量(Meyers Singleton) | 编译器保证线程安全(C++11 起) | 极简 | 只需单一实例,且不需要显式销毁 |
| ④ 构造函数参数化(依赖注入) | 通过外部控制实例生命周期 | 需要改造设计 | 对于需要多实例或测试友好时 |
| ⑤ 模板化单例 | 用模板生成不同类的单例 | 代码量较大 | 需要为多类生成单例 |
⑥ 使用 std::shared_ptr + std::make_shared |
共享所有权,便于在多线程间传递 | 需要关注引用计数 | 需要跨模块共享实例 |
下面以 ① 和 ③ 为主,给出完整实现,并解释其优势与局限。
3. 示例代码
3.1 C++11 std::call_once 版本
#include <mutex>
#include <memory>
#include <iostream>
class Singleton {
public:
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 提供全局访问点
static Singleton& Instance() {
std::call_once(initFlag_, []() {
instance_ = std::unique_ptr <Singleton>(new Singleton());
});
return *instance_;
}
void DoWork() {
std::cout << "Doing work in singleton instance " << this << '\n';
}
private:
Singleton() { std::cout << "Singleton constructed\n"; }
~Singleton() { std::cout << "Singleton destroyed\n"; }
static std::unique_ptr <Singleton> instance_;
static std::once_flag initFlag_;
};
// 静态成员初始化
std::unique_ptr <Singleton> Singleton::instance_;
std::once_flag Singleton::initFlag_;
说明
std::call_once确保 lambda 表达式只执行一次。std::once_flag是一次性标记,内部采用原子操作。unique_ptr自动管理实例生命周期,避免内存泄漏。- 线程安全性得到保证,且无需显式加锁。
3.2 Meyers 单例(静态局部变量)版本
class SingletonMeyers {
public:
static SingletonMeyers& Instance() {
static SingletonMeyers instance; // C++11 起保证线程安全
return instance;
}
void Print() { std::cout << "Meyers Singleton, address: " << this << '\n'; }
private:
SingletonMeyers() { std::cout << "Meyers Singleton constructed\n"; }
~SingletonMeyers() { std::cout << "Meyers Singleton destroyed\n"; }
};
说明
- 静态局部变量在第一次调用时初始化。
- C++11 标准保证了初始化的线程安全。
- 代码极简,无需手动管理内存。
4. 双重检查锁(适用于老版本)
如果你必须在 C++03 环境下编译,可以使用 pthread 或 std::mutex 与 std::atomic(通过 volatile)实现双重检查锁。示例代码略显冗长,核心思路是:
static Singleton* instance = nullptr;
static std::mutex mtx;
Singleton* Singleton::Instance() {
if (!instance) { // 第一次检查
std::lock_guard<std::mutex> lock(mtx);
if (!instance) { // 第二次检查
instance = new Singleton();
}
}
return instance;
}
注意
- 必须使用
std::atomic或volatile以防止编译器优化导致的指令重排。 - 仅在
C++03环境下才需要此方案,现代项目应优先使用 C++11 及以上特性。
5. 如何选择最佳方案?
| 场景 | 推荐方案 |
|---|---|
| 项目基于 C++11+ | std::call_once 或 Meyers 单例(最简洁) |
| 需要在多模块共享 | 通过 std::shared_ptr 或 std::unique_ptr 对实例进行包装 |
| 必须兼容 C++03 | 双重检查锁 + volatile/std::atomic |
| 需要在多线程间安全销毁 | 在程序结束前手动 delete 实例或使用 std::shared_ptr 让引用计数管理 |
6. 常见陷阱
-
静态成员销毁顺序
- 静态局部变量会在
main结束后销毁,若在其他线程仍在使用会导致野指针。 - 解决办法:将实例持有在
std::shared_ptr中或使用std::atexit注册销毁函数。
- 静态局部变量会在
-
递归调用
Instance()- 在构造函数内部再次调用
Instance()可能导致死循环。 - 解决办法:避免在构造函数里访问单例。
- 在构造函数内部再次调用
-
线程安全的内存模型
- 确保使用
std::call_once或 C++11 静态局部变量时,编译器遵循 C++11 内存模型。 - 对旧编译器需显式使用
std::atomic。
- 确保使用
7. 结语
实现线程安全的单例模式并不一定要复杂。只要掌握好 C++11 的 std::call_once 或利用静态局部变量的天然线程安全特性,你就能在几行代码内得到安全、可靠且易于维护的单例。根据项目的具体需求和编译环境,选择最合适的实现方式,即可避免常见的竞态问题,保证程序的稳定性。祝你编码愉快!