在 C++ 中实现单例模式是常见的设计模式之一,目的是让一个类只有一个实例并提供全局访问点。然而,当程序进入多线程环境时,单例的创建过程必须是线程安全的,否则可能导致多个实例被创建,或者出现竞争条件。下面将从几个角度讨论并演示如何在 C++ 中实现线程安全的单例模式。
1. 单例模式的基本实现
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 之后的线程安全
return instance;
}
// 禁止复制与赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
};
- 局部静态对象:
static Singleton instance;在第一次调用instance()时创建,随后每次调用直接返回已创建对象。 - C++11 标准:保证局部静态对象的初始化是线程安全的。即使多个线程同时调用
instance(),编译器会在内部加锁,确保只会创建一次对象。
2. 传统的双重检查锁(DCL)实现
在 C++11 之前,常用的线程安全单例实现是双重检查锁:
#include <mutex>
class Singleton {
public:
static Singleton* instance() {
if (!instance_) { // 第一次检查
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) { // 第二次检查
instance_ = new Singleton();
}
}
return instance_;
}
// 同样禁用复制与赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::mutex mutex_;
};
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;
优点
- 只在第一次需要实例时才加锁,后续访问性能高。
缺点
- 需要手动管理对象生命周期,容易出现内存泄漏或早期销毁。
- 在某些编译器或优化策略下,可能仍存在指令重排导致的线程安全问题(需使用
std::atomic或内存屏障)。
3. 用 std::call_once 实现单例
std::call_once 提供了一种更简洁且安全的方式来保证一次性初始化。
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag_, [](){
instance_ = new Singleton();
});
return *instance_;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static Singleton* instance_;
static std::once_flag initFlag_;
};
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::initFlag_;
std::once_flag只保证第一次调用call_once的 lambda 函数被执行一次。- 与 DCL 相比,
call_once更易读且不需要手动锁。
4. 基于 std::shared_ptr 的单例
如果需要在单例被销毁后还能重新创建,可以使用 std::shared_ptr 并结合 std::weak_ptr:
#include <memory>
#include <mutex>
class Singleton {
public:
static std::shared_ptr <Singleton> instance() {
std::lock_guard<std::mutex> lock(mutex_);
if (auto sp = ptr_.lock()) {
return sp; // 已有实例,直接返回
}
auto sp = std::shared_ptr <Singleton>(new Singleton());
ptr_ = sp; // 记录弱指针
return sp;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
~Singleton() = default;
static std::weak_ptr <Singleton> ptr_;
static std::mutex mutex_;
};
std::weak_ptr <Singleton> Singleton::ptr_;
std::mutex Singleton::mutex_;
- 通过
std::weak_ptr检查实例是否已经存在。 - 如果所有
std::shared_ptr实例都销毁,单例会被释放,随后再次调用instance()可以重新创建。
5. 性能与实现细节对比
| 实现方式 | 线程安全 | 代码复杂度 | 性能 | 适用场景 |
|---|---|---|---|---|
| 局部静态对象(C++11+) | ✔ | 低 | 高 | 需要单例在整个程序生命周期内存在 |
| 双重检查锁(DCL) | ✔(需注意指令重排) | 中 | 中 | 老项目、无法使用 C++11 |
std::call_once |
✔ | 低 | 高 | C++11 及以后,想显式控制初始化 |
std::shared_ptr+std::weak_ptr |
✔ | 高 | 中 | 需要可被销毁并重建的单例 |
6. 常见错误与注意事项
- 复制构造/赋值:一定要显式删除,否则外部可以复制单例实例,导致出现多个实例。
- 对象销毁顺序:如果使用局部静态对象,销毁顺序不确定,可能导致在析构过程中访问已销毁的静态对象。
- 指针悬挂:使用裸指针时要注意生命周期,避免在析构时访问已释放内存。
- 全局变量优先:C++ 运行时全局变量的销毁顺序与线程的结束顺序无关,需谨慎使用全局单例。
7. 小结
- 在 C++11 及以后,推荐使用局部静态对象或
std::call_once来实现单例,既简洁又线程安全。 - 如果需要单例可销毁并在之后重新创建,可以使用
std::shared_ptr与std::weak_ptr组合。 - 对于老项目或特殊需求,双重检查锁仍是可行方案,但需额外关注指令重排与内存屏障。
掌握这些实现技巧后,你就能在任何 C++ 项目中安全、可靠地使用单例模式,既满足全局访问的需求,又兼顾多线程环境的稳定性。