单例模式是一种常用的设计模式,它确保一个类只有一个实例,并提供全局访问点。虽然实现单例模式本身并不复杂,但在多线程环境下保持线程安全则需要仔细处理。下面我们从不同角度阐述如何在 C++ 中实现一个线程安全的单例,并讨论其优缺点。
1. 基本思路
单例需要满足两个核心要求:
- 唯一性:类只能创建一次实例。
- 全局可访问:可以在任何地方访问该实例。
在单线程环境下,最直接的方法是使用静态局部变量:
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 之后的静态局部变量是线程安全的
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
C++11 之后,编译器保证 static 局部变量在第一次访问时的初始化是线程安全的。这样,代码既简洁又高效,成为推荐实现。
2. 传统的“双重检查锁定”实现
在 C++11 之前,常见的做法是使用互斥锁 + 双重检查(Double-Check Locking, DCL):
class Singleton {
public:
static Singleton* getInstance() {
if (!instance) { // 第一层检查(无锁)
std::lock_guard<std::mutex> lock(mtx);
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 mtx;
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
优点:只有第一次创建实例时才会加锁,性能相对较好。
缺点:实现复杂,容易出错;在某些编译器/平台下可能出现指令重排导致的可见性问题;手动管理 new/delete 需要自行考虑异常安全和程序退出时的资源释放。
3. 使用 std::call_once
C++11 提供了 std::call_once 与 std::once_flag,可以简洁地实现一次性初始化:
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(flag, []{ instance.reset(new Singleton()); });
return *instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::unique_ptr <Singleton> instance;
static std::once_flag flag;
};
std::unique_ptr <Singleton> Singleton::instance;
std::once_flag Singleton::flag;
优点:代码简洁、线程安全、避免了手动内存管理。
缺点:需要 unique_ptr 进行资源管理;如果在多线程程序中需要频繁访问实例,仍会有一定的锁开销。
4. 资源释放策略
在多线程程序结束时,单例实例需要正确销毁。常见方案:
- 延迟销毁:使用
static局部变量,程序退出时自动销毁。 - 显式销毁:提供
destroy()方法,手动销毁。需要注意多线程环境下的调用时机。 - 惰性销毁:如
std::shared_ptr+std::weak_ptr组合,只有所有引用失效后才销毁。
5. 性能与可扩展性评估
- 单次初始化成本:
call_once与once_flag的开销通常在几十到几百纳秒,足以满足大多数需求。 - 后续访问成本:使用静态局部变量后,访问几乎无锁;
call_once需要检查once_flag的状态,成本略高。 - 可扩展性:若单例类需要依赖其他服务,建议使用依赖注入或工厂模式,而不是硬编码在单例内部。
6. 小结
- 对于 C++11 及以上版本,推荐使用
static局部变量或std::call_once方式实现线程安全的单例,代码简洁且可靠。 - 对于 C++11 之前的代码,必须小心处理双重检查锁定,确保指令重排和可见性问题得到解决。
- 资源释放时需谨慎,避免野指针或内存泄漏。
通过上述方案,你可以在多线程 C++ 程序中安全、高效地使用单例模式,为系统的全局配置、日志管理、缓存等提供统一入口。