在多线程环境下,单例模式的实现需要保证以下两点:
- 只创建一次实例。
- 多线程并发访问时不产生竞争。
下面给出几种常见的实现方式,并说明各自的优缺点。
1. 双重检查锁(Double‑Checked Locking)
class Singleton {
private:
static std::atomic<Singleton*> instance_;
static std::mutex mutex_;
Singleton() = default; // 私有构造函数
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* getInstance() {
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;
}
};
std::atomic<Singleton*> Singleton::instance_{nullptr};
std::mutex Singleton::mutex_;
优点
- 只在第一次访问时加锁,后续访问性能几乎与单线程无差。
缺点
- 代码较为复杂,容易出现指令重排导致的未定义行为。
- 对于 C++11 之前的编译器(如某些老版本的 GCC)可能不安全。
2. 只使用局部静态变量(Meyers 单例)
class Singleton {
private:
Singleton() = default;
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton& getInstance() {
static Singleton instance; // 线程安全的局部静态初始化
return instance;
}
};
优点
- 代码简洁。
- 从 C++11 开始,局部静态变量的初始化是线程安全的。
- 在销毁时自动析构,避免了内存泄漏。
缺点
- 对于大对象或高频访问的单例,可能在第一次访问时产生较大延迟。
- 在某些环境下(如使用
-fno-threadsafe-statics编译器选项)不保证线程安全。
3. 采用 std::call_once
class Singleton {
private:
Singleton() = default;
static Singleton* instance_;
static std::once_flag initFlag_;
public:
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* getInstance() {
std::call_once(initFlag_, [](){
instance_ = new Singleton();
});
return instance_;
}
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
优点
- 代码既清晰又可靠。
std::call_once只会执行一次初始化,且内部已实现线程安全。- 与
Meyers单例相比,call_once可以在任何函数中使用(不局限于返回引用)。
缺点
- 需要手动管理
instance_的生命周期,可能导致内存泄漏。 - 需要额外的
once_flag成员。
4. 对象销毁与内存泄漏的处理
- Meyers 单例:在程序结束时自动析构。
- 双重检查锁与
call_once:如果需要在程序结束时析构单例,可以使用std::shared_ptr或在atexit注册析构函数。
static std::unique_ptr <Singleton> instance;
static void destroyInstance() {
instance.reset();
}
5. 性能评估
| 方法 | 加锁次数 | 线程安全 | 代码复杂度 |
|---|---|---|---|
| 双重检查锁 | 1(首次) | ✔ | ★★ |
| Meyers 单例 | 0 | ✔ | ★ |
call_once |
1(首次) | ✔ | ★★ |
从性能角度看,Meyers 单例 由于不需要加锁,最适合高频访问的场景;而 双重检查锁 和 call_once 则在需要显式控制实例生命周期时更有优势。
6. 结语
在现代 C++(C++11 及以后)中,最推荐使用 Meyers 单例 或 std::call_once 的实现方式。它们既简洁又保证线程安全,同时避免了手动内存管理带来的风险。若项目使用的编译器不支持 C++11,或需要在更旧的标准下工作,则可以考虑使用双重检查锁,但需注意平台特性与内存模型。
选择哪种实现方式,主要取决于项目对性能、可维护性和生命周期控制的具体需求。