在C++程序中,单例模式是一种常用的设计模式,保证一个类只有一个实例并提供全局访问点。然而,在多线程环境下,如何保证单例的创建既线程安全又高效,是一个值得探讨的问题。本文将从不同角度阐述几种实现方式,并给出代码示例,帮助读者快速掌握。
1. 经典双重检查锁(Double‑Check Locking)
思路
- 先检查实例指针是否为
nullptr,如果不为nullptr直接返回。 - 如果为
nullptr,进入互斥锁保护的区域,再次检查一次,确保没有其他线程已经创建实例。 - 这样可以避免每次获取实例时都要加锁,提高性能。
代码示例
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
if (!instance_) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) { // 第二次检查
instance_ = new Singleton();
}
}
return *instance_;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
注意事项
- C++11 及以后版本的编译器已保证原子性和可见性,双重检查锁可安全使用。
- 需确保
instance_的释放,通常可以在程序退出时手动delete,或使用std::unique_ptr管理。
2. 静态局部变量(Meyer’s Singleton)
思路
- 直接在
getInstance()内使用static局部对象。C++11 起,编译器保证此初始化是线程安全的。 - 代码简洁、无显式锁,推荐使用。
代码示例
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 线程安全的初始化
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
优点
- 无需手动管理锁。
- 延迟初始化,只有首次调用时才创建实例。
- 自动在程序结束时销毁,避免内存泄漏。
缺点
- 对于需要在
main之前访问单例的场景,可能导致“静态初始化顺序问题”。
3. C++17 的 std::call_once 与 std::once_flag
思路
- 使用
std::call_once函数保证只执行一次初始化代码,结合std::once_flag进行同步。 - 适用于需要在多线程环境下进行复杂初始化的情况。
代码示例
#include <mutex>
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(initFlag_, []() {
instance_ = new Singleton();
});
return *instance_;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
何时使用
- 需要在实例创建前做额外操作(如读取配置文件、初始化日志系统等)时,可将这些逻辑放在
call_once的 lambda 中。
4. 现代化实现:使用 std::shared_ptr 与 std::atomic
思路
- 用
std::atomic<std::shared_ptr<Singleton>>来管理实例。 - 在第一次请求时使用
compare_exchange_strong创建实例,后续只需原子读取即可。
代码示例
#include <atomic>
#include <memory>
class Singleton {
public:
static std::shared_ptr <Singleton> getInstance() {
std::shared_ptr <Singleton> expected = nullptr;
if (!instance_.load(std::memory_order_acquire)) {
auto newPtr = std::make_shared <Singleton>();
if (instance_.compare_exchange_strong(expected, newPtr,
std::memory_order_release,
std::memory_order_relaxed)) {
return newPtr;
}
}
return instance_.load(std::memory_order_acquire);
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::atomic<std::shared_ptr<Singleton>> instance_;
};
std::atomic<std::shared_ptr<Singleton>> Singleton::instance_{nullptr};
优点
- 支持多拷贝计数,实例可以在多处被共享。
- 避免了单例本身的销毁问题(由
shared_ptr自动处理)。
5. 何时选用哪种实现?
| 实现方式 | 适用场景 | 复杂度 | 维护成本 |
|---|---|---|---|
| 双重检查锁 | 旧版编译器/需要手动控制 | 中等 | 需要注意内存可见性 |
| Meyer’s Singleton | 绝大多数情况 | 低 | 最推荐 |
call_once |
需要复杂初始化 | 中等 | 代码稍多 |
std::atomic<std::shared_ptr> |
需要共享实例 | 高 | 适合大规模系统 |
6. 结语
线程安全的单例模式在 C++ 发展过程中逐渐趋向简洁与安全。现代标准(C++11 及以后)提供了多种原子操作、线程同步工具,开发者只需根据项目需求选择合适的实现即可。掌握这些模式,不仅能让你编写更可靠的代码,也能在多线程环境下保持程序的高性能与易维护性。祝你编码愉快!