6、单例模式:模式动机与定义、懒汉式与饿汉式实现、线程安全实现(C++11 magic static)、优缺点分析
6.1 模式动机——为什么需要单例?
说实话,单例模式可能是设计模式里最「接地气」的一个。你想想看,一个系统里有些对象只需要一个实例就够了——比如日志管理器、配置中心、线程池、数据库连接池。如果每个模块都 new 一个出来,不仅浪费内存,还会出现数据不一致的问题。
我在项目中遇到过这样一个坑:一个后台服务,日志模块被多个线程同时调用。一开始没做单例,结果每个线程各自创建了一个日志实例,写文件时互相覆盖,日志全乱套了。排查了半天才发现是实例不唯一的问题。从那以后,我对单例模式就格外上心。
说白了,单例模式的核心动机就两条:
- 控制实例数量——确保一个类只有一个对象
- 提供全局访问点——让任何地方都能拿到这个唯一的实例
6.2 模式定义
单例模式的定义其实很简单:保证一个类仅有一个实例,并提供一个访问它的全局访问点。嗯,就这两句话。但实现起来,细节可不少。
我个人的习惯是,先画一张图把整体逻辑理清楚,再动手写代码。下面这张 SVG 图展示了单例模式的核心结构:
6.3 饿汉式实现——简单粗暴
饿汉式,顾名思义,就是「很饿」,程序一启动就把实例创建好。不管后面用不用,先 new 出来再说。
// 饿汉式单例
class Singleton {
public:
static Singleton* getInstance() {
return &instance;
}
void doSomething() {
// 业务逻辑
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton instance; // 类加载时就初始化
};
// 在源文件中定义
Singleton Singleton::instance;
饿汉式的优点很明显:实现简单,天生线程安全。因为实例在 main 函数执行前就创建好了,不存在多线程竞争的问题。
但缺点也很致命:如果这个单例对象很重,或者程序根本没用它,那就白白浪费了资源。我记得有一次接手一个老项目,里面有个配置管理器用的饿汉式,启动时加载了 200MB 的配置文件。结果大部分功能根本不需要这个配置,白白拖慢了启动速度。
6.4 懒汉式实现——按需加载
懒汉式就聪明多了:什么时候用,什么时候创建。第一次调用 getInstance() 时才 new 对象。
// 懒汉式单例(非线程安全)
class Singleton {
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* instance;
};
Singleton* Singleton::instance = nullptr;
这个版本有个大问题——线程不安全。两个线程同时调用 getInstance(),可能都发现 instance 是 nullptr,然后各自 new 了一个对象。这就违背了单例的初衷。
我曾经在一个高并发的网络服务里用过这个版本,上线第一天就出了 bug。两个线程同时创建了日志实例,结果日志文件被交替写入,内容全乱了。嗯,从那以后我再也不敢用裸指针的懒汉式了。
6.5 线程安全实现——C++11 magic static
C++11 引入了一个「魔法」特性:函数局部静态变量的初始化是线程安全的。标准规定,如果多个线程同时首次访问一个静态局部变量,只有一个线程会执行初始化,其他线程会阻塞等待。
利用这个特性,我们可以写出最优雅、最高效的单例实现:
// C++11 magic static 单例(线程安全)
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance; // 线程安全的局部静态变量
return instance;
}
void doSomething() {
// 业务逻辑
}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
这个版本的好处太多了:
- 线程安全——C++11 标准保证,不需要加锁
- 懒加载——第一次调用时才创建
- 代码简洁——没有指针,没有锁,没有 double-check
- 自动析构——程序结束时自动销毁,没有内存泄漏
6.6 三种实现方式对比
| 特性 | 饿汉式 | 懒汉式(裸指针) | Magic Static |
|---|---|---|---|
| 线程安全 | ✅ 天生安全 | ❌ 不安全 | ✅ C++11 保证 |
| 懒加载 | ❌ 启动即创建 | ✅ 按需创建 | ✅ 按需创建 |
| 代码复杂度 | 低 | 中 | 低 |
| 性能开销 | 无运行时开销 | 需加锁(若安全) | 首次调用有开销 |
| 自动析构 | ✅ | ❌ 需手动管理 | ✅ |
| 适用场景 | 轻量对象,必定使用 | 不推荐使用 | 绝大多数场景 |
6.7 优缺点分析
优点:
- 实例唯一性——从根源上保证只有一个对象
- 全局访问——任何地方都能拿到实例,方便
- 延迟初始化——懒汉式可以节省资源
- 避免频繁创建销毁——适合重量级对象
缺点:
- 违反单一职责——既要管理实例,又要做业务逻辑
- 不利于扩展——如果以后需要多个实例,改动很大
- 隐藏依赖关系——全局访问点让模块间耦合变高
- 测试困难——单例的全局状态会影响单元测试
核心总结:单例模式是一把双刃剑。用好了,能优雅地管理全局资源;用不好,会让代码变得难以维护。我个人建议:能用依赖注入就别用单例,非要用单例就选 magic static。记住,单例不是银弹,它只是一个工具。