在多线程环境下,单例模式常常被用来保证一个类只有一个实例,并且可以在任何地方访问。虽然 C++11 之后提供了很多线程安全的工具,但实现一个真正安全且高效的单例仍需要注意细节。下面我们从基本实现到高级优化,逐步拆解常见做法,帮助你在项目中快速落地。
1. 基础实现:局部静态变量(C++11 之选)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后的局部静态变量初始化是线程安全的
return instance;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
要点
- 通过
delete删除拷贝构造和赋值运算符,防止被复制。 static对象在第一次调用时构造,随后只返回同一实例。- C++11 规范保证了初始化的原子性和互斥性,避免了“双重检查锁定”之类的错误。
2. 延迟实例化 + 双重检查锁定(兼容 C++03)
如果你需要在旧编译器(如 C++03)下实现线程安全单例,可以使用双重检查锁定(Double-Checked Locking,DCL):
class Singleton {
public:
static Singleton* instance() {
Singleton* temp = instance_;
if (!temp) {
std::lock_guard<std::mutex> lock(mutex_);
temp = instance_;
if (!temp) {
temp = new Singleton();
instance_ = temp;
}
}
return temp;
}
private:
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 之前,
std::atomic并不适用于指针的可见性保证,必须使用锁。 - 该实现需要手动删除实例,或者在程序退出时依赖系统回收(可能导致顺序不确定)。
3. 现代 C++:std::call_once 与 std::once_flag
std::call_once 是最安全、最简洁的方式,避免手写锁。
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, []() { instance_ = new Singleton(); });
return *instance_;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance_;
static std::once_flag flag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
优点
- 只执行一次初始化,且线程安全。
- 避免了显式锁的性能开销。
- 可与
std::unique_ptr结合,实现自动释放。
4. 延迟销毁:使用 std::unique_ptr 与自定义销毁器
在程序退出时,若单例需要按特定顺序销毁(尤其是跨库依赖),可以通过自定义销毁器:
class Singleton {
public:
static Singleton& instance() {
static std::unique_ptr <Singleton> instance{new Singleton()};
return *instance;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
解释
staticunique_ptr保证对象在程序结束时按main退出顺序销毁。- 只要
instance()先被调用,内存管理就被托付给unique_ptr,不必担心泄漏。
5. 线程安全的懒加载与资源管理
如果单例内部需要管理大量资源,建议分离“单例容器”和“资源加载器”。例如:
class ResourceManager {
public:
static ResourceManager& instance() {
static ResourceManager manager;
return manager;
}
void load(const std::string& key, const std::string& path) {
std::lock_guard<std::mutex> lock(mu_);
// 读取文件、解析等
}
std::shared_ptr <Resource> get(const std::string& key) {
std::lock_guard<std::mutex> lock(mu_);
return resources_[key];
}
private:
ResourceManager() = default;
std::unordered_map<std::string, std::shared_ptr<Resource>> resources_;
std::mutex mu_;
};
核心思路
- 单例本身仅负责容器管理,所有资源访问都通过加锁实现。
- 对于只读访问,可考虑读写锁或原子指针,以提升并发度。
6. 常见陷阱与调试技巧
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 单例被复制 | 未删除拷贝构造/赋值 | 通过 delete 或 =delete |
| 多线程初始化竞态 | 传统 DCL 的指针可见性问题 | 使用 std::call_once 或局部静态变量 |
| 资源泄漏 | 未释放单例 | 依赖静态对象析构或手动 delete |
| 析构顺序错误 | 静态对象跨文件 | 使用 std::unique_ptr 或 atexit 注册 |
调试时可以在 instance() 内打印线程 ID,确认初始化只发生一次。
7. 小结
- C++11+:局部静态变量或
std::call_once是推荐方案,代码最简洁且线程安全。 - 旧标准:双重检查锁定可以实现,但实现更繁琐且容易出错。
- 资源管理:单例可以进一步拆分为资源容器,使用锁或读写锁保证并发安全。
- 销毁顺序:若有跨库依赖,使用
unique_ptr或手动销毁可避免顺序错误。
掌握这些实现模式后,你可以在任何项目中快速构建安全、可维护的单例组件。祝编码愉快!