在多线程环境下,单例模式需要保证同一时刻只有一个实例被创建,并且所有线程都能安全地访问该实例。下面介绍几种常见的实现方式,并比较它们的优缺点。
1. 使用 std::call_once 与 std::once_flag
C++11 标准库提供了 std::call_once 与 std::once_flag,可以在多线程中安全地初始化单例。
#include <mutex>
#include <memory>
class Singleton {
public:
static Singleton& instance() {
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_;
优点
- 线程安全:
std::call_once保证初始化只执行一次。 - 代码简洁:不需要手动锁或原子操作。
- 延迟初始化:仅在第一次调用
instance()时创建。
缺点
- C++11 依赖:只能在支持 C++11 及以后标准的编译器中使用。
- 静态对象:在销毁时可能出现“静态析构顺序问题”,但因为使用
unique_ptr,在程序结束前会被销毁。
2. 双重检查锁(Double-Check Locking)
经典的双重检查锁实现仍然在一些代码库中出现,虽然在 C++11 前不安全,但在 C++11 之后由于内存模型的改进,可以安全使用。
#include <atomic>
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
Singleton* tmp = instance_.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lock(mutex_);
tmp = instance_.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton;
instance_.store(tmp, std::memory_order_release);
}
}
return *tmp;
}
// 禁止拷贝和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
优点
- 延迟初始化:与
call_once相同。 - 可读性:在某些团队中更易于理解。
缺点
- 复杂度高:需要手动管理原子操作与锁。
- 潜在错误:如果不严格遵守
std::memory_order,可能导致数据竞争。
3. 静态局部变量(Meyers’ Singleton)
最简洁、最安全的实现方式是利用 C++11 之后的局部静态变量初始化线程安全的特性。
class Singleton {
public:
static Singleton& instance() {
static Singleton instance;
return instance;
}
// 禁止拷贝和移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
优点
- 代码最短:几行代码即可完成。
- 自动销毁:在程序结束时自动销毁。
- 线程安全:C++11 起保证局部静态变量初始化是线程安全的。
缺点
- 延迟销毁:如果想手动销毁单例,需要额外设计。
- 不可跨编译单元:若在多个源文件中调用,可能导致不同实例,除非把
instance()放在头文件并使用inline或constexpr。
4. 总结与最佳实践
| 实现方式 | 线程安全 | 延迟初始化 | 代码量 | 兼容性 |
|---|---|---|---|---|
std::call_once |
✔ | ✔ | 中等 | C++11+ |
| 双重检查锁 | ✔ | ✔ | 高 | C++11+ |
| Meyers’ Singleton | ✔ | ✔ | 低 | C++11+ |
显式 static 成员 |
✔ | ✔ | 低 | C++11+ |
建议:
- 优先使用 Meyers’ Singleton:最简洁且安全,适合大多数场景。
- 若需要手动销毁或延迟释放,使用
std::call_once与unique_ptr结合。 - 避免使用全局
new/delete:会产生内存泄漏或析构顺序问题。 - 保持单例的不可复制性:删除拷贝构造函数与赋值运算符。
通过以上几种实现,你可以根据项目需求、编译器支持与代码规范选择最合适的单例模式。祝编码愉快!