在 C++ 开发中,单例模式经常被用来确保一个类只有一个实例,并且提供全局访问点。随着多线程程序的普及,传统单例实现往往面临线程安全问题。下面从经典实现、双重检查锁定、C++11 的原子操作以及 std::call_once 等角度,系统地剖析如何在多线程环境下实现线程安全的单例。
1. 经典单例实现(不线程安全)
class Singleton {
private:
static Singleton* instance;
Singleton() {} // 私有构造函数
public:
static Singleton* getInstance() {
if (!instance) {
instance = new Singleton();
}
return instance;
}
};
Singleton* Singleton::instance = nullptr;
此实现缺乏互斥锁,多个线程同时调用 getInstance() 时可能会产生多重实例。
2. 双重检查锁定(DCL)与 std::mutex
#include <mutex>
class Singleton {
private:
static Singleton* instance;
static std::mutex mtx;
Singleton() {}
public:
static Singleton* getInstance() {
if (!instance) { // 第一次检查(无锁)
std::lock_guard<std::mutex> lock(mtx);
if (!instance) { // 第二次检查(有锁)
instance = new Singleton();
}
}
return instance;
}
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
- 优点:只在第一次创建时锁,随后访问无锁,性能较好。
- 缺点:在某些编译器/CPU 体系结构上仍可能出现“可见性”问题(即内存屏障不足导致
instance先被写入,但构造函数未完成),导致其它线程看到不完整的实例。
3. C++11 的 std::atomic 与 std::atomic_thread_fence
#include <atomic>
class Singleton {
private:
static std::atomic<Singleton*> instance;
static std::atomic_flag flag = ATOMIC_FLAG_INIT;
Singleton() {}
public:
static Singleton* getInstance() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (!tmp) {
std::atomic_thread_fence(std::memory_order_acquire);
if (!instance.load(std::memory_order_relaxed)) {
Singleton* newInstance = new Singleton();
instance.store(newInstance, std::memory_order_release);
return newInstance;
}
tmp = instance.load(std::memory_order_acquire);
}
return tmp;
}
};
std::atomic<Singleton*> Singleton::instance{nullptr};
std::atomic_flag Singleton::flag = ATOMIC_FLAG_INIT;
利用原子指针和内存序保证构造完成后,所有线程都能看到完整实例。实现更复杂,但对硬件内存模型兼容性更好。
4. std::call_once 与 std::once_flag(推荐方式)
C++11 引入 std::call_once,为一次性初始化提供了最简洁且安全的机制:
#include <mutex>
class Singleton {
private:
static Singleton* instance;
static std::once_flag flag;
Singleton() {}
public:
static Singleton* getInstance() {
std::call_once(flag, [](){
instance = new Singleton();
});
return instance;
}
};
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::flag;
- 优势
- 代码简洁,易读。
std::call_once采用内部锁或无锁实现,保证只执行一次且线程安全。- 对所有标准实现均有效,无需手动处理内存序。
5. 静态局部变量(C++11 之后即线程安全)
C++11 标准保证局部静态变量在首次进入时初始化是线程安全的,这也是最简洁且安全的单例实现方式:
class Singleton {
private:
Singleton() {}
public:
static Singleton& getInstance() {
static Singleton instance; // 线程安全的局部静态初始化
return instance;
}
};
- 优点:无需显式锁或原子操作。
- 缺点:无法自定义析构顺序(除非使用
atexit注册),但对大多数应用足够。
6. 资源释放与单例的生命周期
单例常常伴随全局资源(文件句柄、数据库连接等)。在多线程环境下,优雅的释放机制尤为重要:
-
使用
std::shared_ptrstd::shared_ptr <Singleton> getInstance() { static std::shared_ptr <Singleton> instance(new Singleton, [](Singleton* p){ delete p; }); return instance; }通过引用计数自动释放。
-
使用
std::unique_ptr与atexitstatic std::unique_ptr <Singleton> instance; static void init() { instance.reset(new Singleton()); } static void destroy() { instance.reset(); }在
main()开始时atexit(destroy)注册,程序结束时自动销毁。
7. 小结
| 方法 | 线程安全 | 复杂度 | 适用场景 |
|---|---|---|---|
原始指针 + if (!instance) |
❌ | 低 | 单线程 |
双重检查锁定 + std::mutex |
✅(但有潜在可见性问题) | 中 | 性能敏感 |
std::atomic + 内存序 |
✅ | 高 | 对硬件模型要求严格 |
std::call_once + std::once_flag |
✅ | 低 | 推荐 |
| 局部静态变量 | ✅ | 极低 | 推荐(C++11 及以后) |
最佳实践:除非对性能有极端要求,首选
std::call_once或局部静态变量。它们既简洁又完全符合标准,几乎可以在所有平台上无缝工作。
进一步阅读
- Scott Meyers – Effective Modern C++
- Herb Sutter – C++ Concurrency in Action
- ISO/IEC 14882:2017 (C++17) – §6.7 “Static Initialization”
通过掌握上述技术,你可以在 C++ 中稳健地实现线程安全的单例模式,并在多线程项目中获得更可靠的全局资源管理。祝你编码愉快!