单例模式是设计模式中创建型模式 的核心之一,核心目标是:保证一个类在程序生命周期内仅有一个实例 ,并提供一个全局统一的访问入口。懒汉模式(Lazy Singleton)和饿汉模式(Hungry Singleton,也译 "饥饿模式")是单例模式最常见的两种实现方式,核心区别在于实例初始化的时机,以及由此衍生的线程安全、资源占用特性。
一、饿汉模式(饥饿模式)
1. 核心思想
"饿" 意味着类加载时就立即初始化实例,不管后续是否会用到这个实例。实例的创建发生在程序启动 / 类加载阶段,而非第一次调用访问方法时。
2. 实现(C++ 示例)
#include <iostream>
using namespace std;
class HungrySingleton {
private:
// 1. 私有构造函数:禁止外部创建实例
HungrySingleton() {
cout << "饿汉模式:实例已初始化(类加载时)" << endl;
}
// 2. 私有静态成员变量:类加载时直接初始化实例(全局唯一)
static HungrySingleton* instance;
public:
// 3. 公有静态方法:提供全局访问入口
static HungrySingleton* getInstance() {
return instance; // 仅返回已创建的实例
}
// 禁止拷贝/赋值(保证单例唯一性)
HungrySingleton(const HungrySingleton&) = delete;
HungrySingleton& operator=(const HungrySingleton&) = delete;
};
// 类外初始化静态成员(类加载阶段执行,仅一次)
HungrySingleton* HungrySingleton::instance = new HungrySingleton();
// 测试
int main() {
cout << "程序启动,尚未调用getInstance()" << endl;
HungrySingleton* s1 = HungrySingleton::getInstance();
HungrySingleton* s2 = HungrySingleton::getInstance();
cout << "s1 和 s2 是否指向同一实例:" << (s1 == s2) << endl; // 输出 1(true)
return 0;
}
3. 核心特点
| 优点 | 缺点 |
|---|---|
| ① 天然线程安全:类加载由编译器 / 操作系统保证原子性,仅初始化一次,无多线程竞态问题;② 实现简单,无额外同步开销; | ① 资源提前占用:若实例初始化耗资源(如大内存、网络连接)且全程未使用,造成资源浪费;② 无法处理 "依赖初始化顺序" 问题:若单例依赖其他未初始化的全局变量,可能导致初始化失败; |
4. 适用场景
- 实例初始化开销小、大概率会被使用的场景;
- 对线程安全要求高,且不想引入同步锁的场景。
二、懒汉模式
1. 核心思想
"懒" 意味着延迟初始化 :实例不会在类加载时创建,而是第一次调用 getInstance() 方法时才初始化,做到 "用的时候才创建"。
2. 实现(C++ 示例,分版本)
版本 1:基础版(非线程安全)
#include <iostream>
using namespace std;
class LazySingleton {
private:
LazySingleton() {
cout << "懒汉模式:实例首次初始化(调用getInstance时)" << endl;
}
// 静态成员指针:初始化为空,延迟创建
static LazySingleton* instance;
public:
static LazySingleton* getInstance() {
if (instance == nullptr) { // 第一次调用时创建
instance = new LazySingleton();
}
return instance;
}
// 禁止拷贝/赋值
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
};
// 初始化为空
LazySingleton* LazySingleton::instance = nullptr;
// 测试
int main() {
cout << "程序启动,instance 为空" << endl;
LazySingleton* s1 = LazySingleton::getInstance(); // 首次调用,创建实例
LazySingleton* s2 = LazySingleton::getInstance(); // 直接返回已有实例
cout << "s1 和 s2 是否指向同一实例:" << (s1 == s2) << endl; // 输出 1
return 0;
}
问题 :多线程同时调用 getInstance() 时,可能进入 if (instance == nullptr) 分支,导致创建多个实例,破坏单例。
#include <iostream>
#include <mutex> // 引入互斥锁
using namespace std;
class LazySingleton {
private:
LazySingleton() {}
// C++11 后用 volatile/atomic 防止编译器优化导致的指令重排
static atomic<LazySingleton*> instance;
static mutex mtx; // 互斥锁
public:
static LazySingleton* getInstance() {
// 第一次检查:避免每次调用都加锁(提升性能)
LazySingleton* tmp = instance.load(memory_order_relaxed);
if (tmp == nullptr) {
lock_guard<mutex> lock(mtx); // 加锁
// 第二次检查:防止加锁期间其他线程已创建实例
tmp = instance.load(memory_order_relaxed);
if (tmp == nullptr) {
tmp = new LazySingleton();
instance.store(tmp, memory_order_relaxed);
cout << "懒汉模式:线程安全版实例初始化" << endl;
}
}
return tmp;
}
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
};
// 初始化静态成员
atomic<LazySingleton*> LazySingleton::instance = nullptr;
mutex LazySingleton::mtx;
// 测试多线程(简化示例)
int main() {
LazySingleton* s1 = LazySingleton::getInstance();
LazySingleton* s2 = LazySingleton::getInstance();
cout << "s1 和 s2 是否同一实例:" << (s1 == s2) << endl;
return 0;
}
版本 3:C++11 极简线程安全版(局部静态变量)
C++11 标准规定:局部静态变量的初始化是线程安全的(编译器保证仅初始化一次),无需手动加锁,是懒汉模式的最优实现:
#include <iostream>
using namespace std;
class LazySingleton {
private:
LazySingleton() {
cout << "懒汉模式:C++11 极简版实例初始化" << endl;
}
public:
// 局部静态变量:第一次调用时初始化,线程安全
static LazySingleton& getInstance() {
static LazySingleton instance;
return instance; // 返回引用,避免内存泄漏
}
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
};
// 测试
int main() {
LazySingleton& s1 = LazySingleton::getInstance();
LazySingleton& s2 = LazySingleton::getInstance();
cout << "s1 和 s2 是否同一实例:" << (&s1 == &s2) << endl; // 输出 1
return 0;
}
3. 核心特点
| 优点 | 缺点 |
|---|---|
| ① 延迟加载:仅在使用时初始化,节省资源;② 灵活:可处理依赖初始化顺序的问题; | ① 基础版线程不安全,需额外处理同步(加锁 / 依赖 C++11 特性);② 加锁版存在轻微性能开销(第一次调用后无影响); |
4. 适用场景
- 实例初始化开销大、可能不会被使用的场景(如配置类、工具类);
- 对资源占用敏感,希望 "按需创建" 的场景。
三、懒汉 vs 饿汉 核心对比
| 维度 | 饿汉模式 | 懒汉模式 |
|---|---|---|
| 初始化时机 | 类加载时(程序启动阶段) | 第一次调用 getInstance() 时 |
| 线程安全 | 天然安全(无需额外处理) | 基础版不安全,需加锁 / C++11 特性 |
| 资源占用 | 提前占用,可能浪费 | 延迟占用,按需分配 |
| 实现复杂度 | 简单(无同步逻辑) | 稍复杂(需处理线程安全) |
| 内存泄漏风险 | 静态指针版需手动释放(或用智能指针) | 局部静态版无泄漏,指针版需注意 |
四、总结
- 优先选饿汉模式:实现简单、线程安全,适合实例轻量、必被使用的场景;
- 选懒汉模式(C++11 局部静态版):资源敏感、实例可能未被使用的场景,极简且线程安全;
- 无论哪种模式,都要禁用拷贝构造和赋值运算符,避免通过拷贝创建新实例;
- 单例模式的本质是 "控制实例数量",需根据资源开销、线程场景选择实现方式。
饿汉模式和懒汉模式的优缺点对比
单例模式的应用场景有哪些?
除了单例模式,C++还有哪些常用的设计模式?