在多线程环境下,单例模式的实现往往需要保证一次性初始化以及线程安全。传统的 if (instance == nullptr) { create(); } 方案在多线程下容易产生竞争,需要加锁,导致性能下降。下面给出几种现代 C++(C++11 及以后)实现单例模式的高效方案,并对比其优劣。
1. 样板代码:Meyers 单例
class Logger {
public:
static Logger& instance() {
static Logger instance; // 只在第一次调用时初始化
return instance;
}
void log(const std::string& msg) { /* 记录日志 */ }
private:
Logger() = default;
~Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};
- 线程安全:自 C++11 起,
static局部变量的初始化是线程安全的。 - 性能:只在第一次访问时产生一次锁,后续调用几乎无锁。
- 缺点:实例无法显式销毁,依赖程序结束时自动析构;在某些嵌入式或资源有限环境中不够灵活。
2. 双重检查锁(DCL)配合 std::call_once
class Config {
public:
static Config& get() {
std::call_once(initFlag, []() { instancePtr = new Config; });
return *instancePtr;
}
private:
Config() = default;
static std::once_flag initFlag;
static Config* instancePtr;
};
std::once_flag Config::initFlag;
Config* Config::instancePtr = nullptr;
- 优势:使用
std::call_once可以避免多线程环境下的重复初始化,保证只创建一次。 - 缺点:手动管理内存,容易出现泄漏;不支持显式销毁。
3. 智能指针 + 延迟销毁
class Settings {
public:
static std::shared_ptr <Settings> instance() {
static std::shared_ptr <Settings> ptr;
static std::once_flag flag;
std::call_once(flag, []() { ptr = std::make_shared <Settings>(); });
return ptr;
}
private:
Settings() = default;
};
- 优势:通过
shared_ptr自动管理生命周期,支持显式销毁或提前释放。 - 缺点:每次返回
shared_ptr都会产生一次引用计数的原子操作,微量性能损耗。
4. 预先实例化(静态构造函数)
如果单例的构造开销不大,可以在程序启动时就创建实例,避免运行时延迟。
class Cache {
public:
static Cache& get() { return instance; }
private:
Cache() { /* 预加载缓存 */ }
static Cache instance;
};
Cache Cache::instance;
- 优势:构造时机明确,线程安全性由编译器保证。
- 缺点:无法延迟加载;如果构造失败,程序可能无法正常启动。
5. 何时选择哪种实现
| 场景 | 推荐实现 | 说明 |
|---|---|---|
| 需要在第一次使用时延迟初始化 | Meyers 单例 | 简洁,性能好,适合大多数场景 |
| 需要显式销毁或在多次使用后释放资源 | 智能指针 + call_once |
自动管理内存,适合资源受限环境 |
| 需要预先构造,避免运行时延迟 | 静态实例化 | 适合构造成本低、可预知的单例 |
| 需要多线程安全且不想使用局部静态 | 双重检查锁 | 传统做法,适用于旧编译器或特殊需求 |
6. 常见陷阱与建议
- 析构顺序:若单例持有全局资源,务必确保析构顺序正确,避免在其他全局对象析构时使用已销毁的单例。
- 递归调用:不要在单例构造函数或析构函数内部调用
instance(),这会导致死锁或重复初始化。 - 跨 DLL / SO:在多模块编译时,使用
Meyers 单例可能会产生多份实例。可通过显式导出实例或使用std::call_once管理全局状态。 - 懒加载:如果单例包含大量缓存或数据库连接,建议使用 懒加载(在第一次需要时再创建)或 双重检查锁 以降低启动成本。
结语
现代 C++(C++11 以后)提供了多种简洁且线程安全的单例实现。最常用的是 Meyers 单例,因为它既安全又性能优异,且代码最简洁。然而在特定场景下(如需要显式销毁、跨模块共享或资源限制),使用 std::call_once 配合 std::shared_ptr 或手动指针也很合适。根据实际需求挑选最适合的实现,既能保证线程安全,又能保持代码的可维护性。