在多线程环境下,单例模式的实现往往需要考虑并发访问导致的竞争条件。下面给出一种既简单又高效、兼顾C++11标准下原子操作与懒初始化的实现方式。
1. 懒初始化 + C++11 的 std::call_once
C++11 引入了 std::call_once 与 std::once_flag,这两个工具可以保证函数体只执行一次,且线程安全。示例代码如下:
#include <iostream>
#include <mutex>
class Singleton
{
public:
// 获取单例实例的静态接口
static Singleton& instance()
{
std::call_once(initFlag, [](){
// 这里会在多线程环境下只执行一次
ptr = new Singleton();
});
return *ptr;
}
// 防止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 示例业务方法
void doSomething()
{
std::cout << "Doing something with address: " << this << std::endl;
}
private:
Singleton() { std::cout << "Singleton constructor\n"; }
~Singleton() { std::cout << "Singleton destructor\n"; }
static Singleton* ptr;
static std::once_flag initFlag;
};
// 静态成员定义
Singleton* Singleton::ptr = nullptr;
std::once_flag Singleton::initFlag;
优点
- 线程安全:
std::call_once内部使用原子操作,避免了传统的mutex+double-checked locking的复杂性。 - 延迟初始化:实例化仅在第一次调用
instance()时发生,节省启动时资源。 - 性能:第一次初始化后,后续获取实例仅需一次原子检查,无需加锁。
2. 静态局部变量(Meyers Singleton)
C++11 规范保证了局部静态变量的初始化是线程安全的。因此可以采用更简洁的方式:
class Singleton
{
public:
static Singleton& instance()
{
static Singleton instance; // C++11 线程安全
return instance;
}
// 其余与上面相同
...
};
注意
- 这种方式在程序退出时,析构函数会被调用(除非程序异常终止)。
- 若单例对象管理资源需要在程序终止前显式释放,可结合
std::shared_ptr或手动销毁。
3. 结合智能指针的懒加载
若单例需要在销毁前做特定操作(如写日志、关闭网络等),可以用 std::shared_ptr 与自定义删除器:
class Singleton
{
public:
static std::shared_ptr <Singleton> instance()
{
std::call_once(initFlag, [](){
ptr = std::shared_ptr <Singleton>(new Singleton(),
[](Singleton* p){ /* 自定义清理逻辑 */ delete p; });
});
return ptr;
}
...
private:
static std::shared_ptr <Singleton> ptr;
static std::once_flag initFlag;
};
4. 性能考虑
- 单例实例大小:保持单例对象体积小,避免不必要的成员。
- 访问方式:若只读访问,考虑把成员设为
constexpr或const,减少运行时开销。 - 构造成本:若构造非常昂贵,可使用异步预热技术(线程池提前初始化)。
5. 典型使用场景
| 场景 | 推荐实现 |
|---|---|
| 需要延迟初始化,且只读 | Meyers Singleton |
| 需要在多线程启动时完成复杂资源分配 | call_once + once_flag |
| 需要在程序结束时做自定义清理 | shared_ptr + 自定义删除器 |
6. 常见陷阱
- 非原子操作:手动实现
double-checked locking时需使用std::atomic,否则会出现指令重排导致的访问错误。 - 对象销毁顺序:全局静态对象在程序结束时按逆序销毁,若单例引用了其他静态对象,可能导致悬空引用。
- 异常安全:若构造函数抛异常,
std::call_once会重新尝试初始化;若使用静态局部变量,异常会导致后续访问仍然失败。
小结
在 C++11 及以后版本,最推荐的线程安全单例实现是使用 std::call_once 与 std::once_flag,或更简洁的静态局部变量方式。两种实现均已被标准库证明为线程安全,且代码简洁易维护。根据业务需求选择合适的实现即可。