单例模式(Singleton)保证一个类只有一个实例,并提供全局访问点。在多线程环境下实现线程安全的单例,需要避免竞争条件和保证实例初始化的原子性。下面从 C++11 开始,逐步介绍几种常见实现方式,并讨论它们的优缺点与使用场景。
1. Meyers 单例(局部静态变量)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 之后保证线程安全
return instance;
}
// 禁止拷贝构造和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {} // 私有构造函数
~Singleton() {}
};
-
优点
- 代码最简洁,使用
static局部变量的初始化在 C++11 之后已保证线程安全。 - 延迟加载(首次调用时才创建实例)。
- 无需手动销毁,程序退出时系统自动释放。
- 代码最简洁,使用
-
缺点
- 如果实例需要在程序退出前手动销毁(例如依赖顺序),可能需要更复杂的手段。
- 对于极其早期的 C++ 标准(C++03 或之前),需要手动实现线程同步。
2. 双重检查锁(Double-Check Locking)
class Singleton {
public:
static Singleton* getInstance() {
if (!instance) { // 第一次检查
std::lock_guard<std::mutex> lock(mtx);
if (!instance) { // 第二次检查
instance = new Singleton();
}
}
return instance;
}
// 其他同上
private:
Singleton() {}
static Singleton* instance;
static std::mutex mtx;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
-
优点
- 兼容 C++03,适用于老旧编译器。
- 第一次调用后,后续访问不再加锁,性能好。
-
缺点
- 需要确保
instance的写入对所有线程可见,常用std::atomic<Singleton*>或std::once_flag替代手动锁。 - 实现易错,维护成本高。
- 需要确保
3. std::call_once 与 std::once_flag
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag, [](){ instance = new Singleton(); });
return *instance;
}
// 其他同上
private:
Singleton() {}
static Singleton* instance;
static std::once_flag initFlag;
};
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::initFlag;
-
优点
- 更直观的单次初始化语义,编译器实现保证线程安全。
- 适用于 C++11 及以后,兼容
Meyers单例实现。
-
缺点
- 需要手动删除
instance,如果在多线程环境中释放资源,仍需同步。
- 需要手动删除
4. std::shared_ptr 与 std::weak_ptr 组合
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
std::lock_guard<std::mutex> lock(mtx);
if (auto ptr = instance.lock()) { // 先尝试获取已存在实例
return ptr;
}
instance = std::shared_ptr <Singleton>(new Singleton());
return instance;
}
// 其他同上
private:
Singleton() {}
static std::weak_ptr <Singleton> instance;
static std::mutex mtx;
};
std::weak_ptr <Singleton> Singleton::instance;
std::mutex Singleton::mtx;
-
优点
- 通过
shared_ptr自动管理生命周期,避免手动delete。 weak_ptr让单例可以在所有引用失效后被销毁,适用于需要在多次使用后释放资源的场景。
- 通过
-
缺点
- 每次访问都需要加锁,性能略低。
- 需要关注引用计数的同步问题。
5. 对象销毁顺序与全局析构器
在多线程程序中,若单例需要在程序退出前手动销毁(例如释放文件句柄、网络连接等),可以使用 Meyers 单例 并结合 std::atexit 或自定义全局析构器:
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
static void destroy() {
// 如果使用静态局部变量,系统会自动销毁
// 这里可以添加自定义清理逻辑
}
private:
Singleton() {}
};
int main() {
Singleton::getInstance(); // 触发实例创建
std::atexit(&Singleton::destroy);
return 0;
}
6. 何时选择哪种实现?
| 场景 | 推荐实现 | 说明 |
|---|---|---|
| C++11 及以后 | Meyers 单例 | 最简洁、延迟加载、线程安全 |
| 需要自定义销毁顺序 | Meyers + atexit |
可控制析构时机 |
| 兼容旧标准 | 双重检查锁或 call_once |
手动实现线程安全 |
| 需要在多次使用后释放 | shared_ptr/weak_ptr |
自动管理生命周期 |
7. 小结
- 线程安全:C++11 之后
static局部变量的初始化已保证原子性,推荐使用 Meyers 单例。 - 延迟加载:所有实现默认在第一次访问时创建实例,避免不必要的资源占用。
- 销毁顺序:若单例资源需手动释放,最好在
main结束前调用atexit或使用shared_ptr。
通过上述实现方式,开发者可以根据项目需求、编译器版本以及资源管理策略,选取最合适的线程安全单例实现。