单例模式是软件设计中的一种常见模式,用于确保一个类只有一个实例,并为全局访问点提供共享资源。在C++中实现线程安全的单例模式,需要兼顾懒加载、销毁顺序和跨线程访问。下面给出几种可行的实现方式,并对其优缺点进行分析。
1. Meyer’s Singleton(局部静态变量)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 起保证线程安全的初始化
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() { /* 可能的初始化 */ }
~Singleton() { /* 可能的清理 */ }
};
优点
- 简洁:只需一行代码即可实现线程安全。
- 延迟初始化:对象在第一次使用时才会创建。
- 销毁顺序:在程序结束时自动销毁,避免手动销毁顺序的问题。
缺点
- 无法提前销毁:如果想在程序某个阶段销毁实例,需要使用指针或其它技术。
- 测试不友好:单例的全局状态可能导致单元测试变得复杂。
2. 双重检查锁(Double-Check Locking)
#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;
}
// 其它接口
private:
Singleton() {}
~Singleton() {}
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
优点
- 仅在第一次实例化时才加锁,后续调用几乎无锁耗费。
- 兼容 C++11 以前的编译器,若需要更细粒度控制。
缺点
- 代码更繁琐,需要手动管理指针与析构。
- 若不使用
std::atomic,可能产生数据竞争。 - 仍需手动销毁实例,导致顺序问题。
3. 用 std::call_once
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, [](){
instance_ = new Singleton();
});
return *instance_;
}
// 其它接口
private:
Singleton() {}
~Singleton() {}
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
优点
- 简洁且线程安全;只在第一次调用时执行 lambda。
- 与
Meyer's Singleton相比,允许在需要时手动销毁。
缺点
- 同样需要手动销毁实例,导致析构顺序问题。
4. 基于 std::shared_ptr 的懒加载
#include <memory>
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::call_once(flag_, [](){
instance_ = std::shared_ptr <Singleton>(new Singleton);
});
return instance_;
}
// 其它接口
private:
Singleton() {}
~Singleton() {}
static std::shared_ptr <Singleton> instance_;
static std::once_flag flag_;
};
std::shared_ptr <Singleton> Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
优点
std::shared_ptr自动管理生命周期,销毁时机可根据引用计数决定。- 对于需要在多模块间共享实例且生命周期不可预测的场景更友好。
缺点
- 每次访问都需要进行
shared_ptr的拷贝构造,略微增加开销。
选型建议
| 场景 | 推荐实现 |
|---|---|
| 需要全局唯一实例且不关心销毁顺序 | Meyer’s Singleton |
| 需要在程序某一时刻显式销毁 | std::call_once + 指针 |
| 需要在多线程环境下做细粒度的销毁控制 | std::shared_ptr + call_once |
| 编译器不支持 C++11 | 双重检查锁(使用 std::atomic / 原子操作) |
5. 小结
C++11 之后,利用局部静态变量的线程安全初始化可以让单例实现变得极其简洁且安全。若需要更灵活的生命周期管理,std::call_once 结合指针或 std::shared_ptr 提供了更高的可定制性。无论哪种实现,核心原则是:避免全局状态泄漏,控制好实例的创建与销毁顺序,确保多线程访问的原子性。希望这篇文章能帮助你在项目中选型并实现一个健壮的线程安全单例。