在多线程环境下,确保单例对象只被创建一次,并且对所有线程可见,通常被称为“线程安全单例”。下面介绍几种常见实现方式,并讨论它们的优缺点。
1. C++11 及以后:使用 std::call_once 和 std::once_flag
C++11 引入了线程同步原语 std::call_once 和 std::once_flag,可以保证某个函数只被调用一次。实现单例的典型代码如下:
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, []() {
instance.reset(new Singleton);
});
return *instance;
}
// 禁止拷贝和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~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;
优点
- 简洁:不需要手写双重检查锁定(Double-Checked Locking)等复杂代码。
- 性能:
call_once只在第一次调用时产生锁,随后访问无需加锁。 - 可移植:标准库实现保证在所有支持 C++11 的编译器上都能工作。
缺点
- 销毁顺序:如果使用
unique_ptr,在程序结束时单例会被销毁;若需要在特定顺序销毁,可能需要手动管理。
2. C++11:局部静态变量(Meyers 单例)
从 C++11 开始,局部静态变量的初始化是线程安全的。实现方式最简洁:
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 线程安全初始化
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
优点
- 代码最短:无须任何同步原语,直接利用语言特性。
- 销毁顺序:局部静态在程序结束时会自动销毁,顺序由编译器决定,符合 C++ 的销毁顺序规则。
缺点
- 懒加载:如果单例在程序启动前就被使用,可能导致延迟。
- 异常安全:如果构造函数抛出异常,后续
getInstance()调用会再次尝试构造,直到成功。
3. 传统实现:双重检查锁定(Double-Checked Locking)
在 C++11 之前,常见的做法是使用互斥锁和双重检查。示例代码:
#include <mutex>
class Singleton {
public:
static Singleton* getInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mutex_);
if (instance == nullptr) {
instance = new Singleton();
}
}
return instance;
}
// 其它成员函数...
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance;
static std::mutex mutex_;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex_;
优点
- 可在 C++98/03 环境中使用:不依赖 C++11 特性。
缺点
- 难以保证正确性:在缺乏强内存模型支持时,可能导致数据竞争。
- 性能成本:每次访问都需要检查指针,虽然锁是可读锁,但仍有一定开销。
4. 现代化方案:使用 std::shared_ptr + std::atomic
如果单例需要在多个线程之间共享并可能被析构(如插件系统),可以使用原子操作和 shared_ptr:
#include <atomic>
#include <memory>
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
std::shared_ptr <Singleton> temp = instance.load(std::memory_order_acquire);
if (!temp) {
std::lock_guard<std::mutex> lock(mutex_);
temp = instance.load(std::memory_order_relaxed);
if (!temp) {
temp = std::shared_ptr <Singleton>(new Singleton());
instance.store(temp, std::memory_order_release);
}
}
return temp;
}
// ...
private:
Singleton() = default;
~Singleton() = default;
static std::atomic<std::shared_ptr<Singleton>> instance;
static std::mutex mutex_;
};
std::atomic<std::shared_ptr<Singleton>> Singleton::instance{nullptr};
std::mutex Singleton::mutex_;
优点
- 可析构:当所有引用消失时,单例会自动析构,适用于需要动态资源管理的场景。
- 线程安全:使用
std::atomic和std::memory_order保证可见性。
缺点
- 实现更复杂:需要理解原子操作与内存序。
- 性能开销:虽然在没有竞争时几乎无锁,但在高并发下仍有一定代价。
5. 选择建议
| 场景 | 推荐实现 |
|---|---|
| 只需要单例且在 C++11 及以后 | std::call_once + unique_ptr 或局部静态(Meyers) |
| 需要在旧编译器 (C++03) 上编译 | 双重检查锁定 + 互斥锁 |
| 单例需要可析构、可被多线程共享 | std::shared_ptr + std::atomic |
| 代码简洁且不关心销毁顺序 | 局部静态(Meyers) |
6. 小结
线程安全单例是 C++ 并发编程中的经典问题。随着标准库的不断完善,C++11 之后的实现变得极为简洁且高效。开发者应根据项目需求(编译器支持、资源生命周期、性能要求)选择最合适的实现方式,并遵循现代 C++ 的最佳实践,避免不必要的手写锁与指针操作,以提升代码的可读性和安全性。