一、饿汉模式 (Eager Initialization)
特点:类加载时就立即创建实例
实现代码:
class Singleton {
private:
static Singleton* instance; // 静态成员
Singleton() {} // 私有构造函数
~Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton* getInstance() {
return instance; // 直接返回已创建的实例
}
};
// 关键:类外初始化时直接创建实例
Singleton* Singleton::instance = new Singleton();
优点:
-
线程安全:实例在程序启动时就创建,多线程访问时不会有竞争问题
-
简单直观:实现简单,不需要考虑线程同步
-
调用效率高 :
getInstance()直接返回指针,没有锁开销
缺点:
-
资源占用早:即使不使用也会占用内存
-
启动慢:如果构造复杂,会影响程序启动速度
-
依赖顺序问题:多个饿汉单例的初始化顺序不确定
二、饱汉模式 (Lazy Initialization)
特点:第一次使用时才创建实例
基础版本(非线程安全):
class Singleton {
private:
static Singleton* instance;
Singleton() {}
~Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton* getInstance() {
if (instance == nullptr) { // 第一次检查
instance = new Singleton(); // 创建实例
}
return instance;
}
};
Singleton* Singleton::instance = nullptr; // 初始化为空
线程安全版本(双检锁):
#include <mutex>
class Singleton {
private:
static Singleton* instance;
static std::mutex mtx;
Singleton() {}
~Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton* getInstance() {
if (instance == nullptr) { // 第一次检查(避免每次加锁)
std::lock_guard<std::mutex> lock(mtx); // 加锁
if (instance == nullptr) { // 第二次检查(确保只创建一次)
instance = new Singleton();
}
}
return instance;
}
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
C++11 之后更简洁的线程安全版本:
class Singleton {
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 保证静态局部变量初始化是线程安全的
return instance;
}
};
三、对比表格
| 特性 | 饿汉模式 | 传统饱汉模式(双检锁) | C++11静态局部变量 |
|---|---|---|---|
| 创建时机 | 程序启动时 (main函数之前) | 第一次调用 getInstance() 时 |
第一次调用 getInstance() 时 |
| 线程安全 | ✅ 天然线程安全 (静态初始化阶段) | ✅ 需要双检锁等机制 (手动实现) | ✅ 编译器保证线程安全 (C++11标准规定) |
| 资源占用 | ❌ 启动即占用内存 (即使不用) | ✅ 使用时才占用内存 | ✅ 使用时才占用内存 |
| 启动速度 | ⚠️ 可能较慢 (如果构造复杂) | ✅ 启动快 | ✅ 启动快 |
| 实现复杂度 | ✅ 简单 | ❌ 复杂 (需正确实现双检锁) | ✅ 非常简洁 (一行代码) |
| 性能开销 | ✅ 调用无锁开销 | ⚠️ 有锁检查开销 (即使已初始化) | ✅ 几乎无开销 (编译器优化) |
| 销毁控制 | ❌ 需手动管理 | ❌ 需手动管理 | ✅ 自动管理 (程序结束时) |
| C++版本要求 | C++98 | C++98(需要锁) C++11(原子操作) | C++11及以上 |
| 内存泄漏风险 | ⚠️ 需正确释放 | ⚠️ 需正确释放 | ✅ 无泄漏风险 |
| 代码示例 | cpp<br>static T* instance = new T();<br> |
cpp<br>if (!instance) {<br> lock();<br> if (!instance) {<br> instance = new T();<br> }<br>}<br> |
cpp<br>static T instance;<br>return instance;<br> |
| 适用场景 | 1. 实例小、构造简单 2. 程序必用 3. 多线程环境 4. C++98项目 | 1. 实例大、构造复杂 2. 可能不用 3. C++98项目 4. 需要延迟初始化 | 绝大多数场景 1. C++11+项目 2. 需要延迟初始化 3. 要求代码简洁 4. 需要自动清理 |
性能对比数据
| 操作 | 饿汉模式 | 双检锁饱汉 | C++11静态局部变量 |
|---|---|---|---|
| 第一次调用 | 0-10 ns | 50-100 ns | 20-50 ns |
| 后续调用 | 0-5 ns | 10-20 ns | 0-5 ns |
| 内存占用 | 立即占用 | 延迟占用 | 延迟占用 |
| 线程竞争 | 无竞争 | 有锁竞争 | 首次有竞争,后续无 |
四、选择建议
使用饿汉模式当:
-
单例对象小,构造简单
-
程序运行期间一定会用到
-
对性能要求高,希望无锁访问
-
多线程环境下不想处理同步问题
使用饱汉模式当:
-
单例对象大,构造复杂或耗时
-
可能整个程序都不会用到该实例
-
对启动速度有要求
-
使用 C++11 或以上版本(可用静态局部变量)