设计模式是软件工程中沉淀的 "代码兵法",而单例模式是最常用的创建型设计模式之一。
它的核心目标是:保证一个类在整个程序生命周期中只有一个实例,并提供全局访问点,适用于配置管理、资源池、日志管理等需要全局唯一实例的场景。
本文将详细讲解单例模式的两种核心实现(饿汉模式、懒汉模式),包括设计思路、代码实现、优缺点及线程安全优化。
一、单例模式的核心价值
在复杂系统中,单例模式能解决以下关键问题:
- 资源统一管理:如服务器配置文件读取,单例对象统一加载配置,避免多实例重复读取 / 修改导致的数据不一致;
- 性能优化:避免频繁创建 / 销毁重量级对象(如数据库连接、网络套接字),降低系统开销;
- 全局访问:提供统一的实例访问入口,简化模块间的交互。
单例模式的实现需满足三个核心约束:
- 构造函数私有化,禁止外部直接创建对象;
- 禁用拷贝构造和赋值运算符,防止通过拷贝生成多实例;
- 提供静态成员函数,作为获取唯一实例的全局入口。
二、饿汉模式:程序启动即初始化
1. 核心思路
"饿汉" 顾名思义 ------程序启动时(main 函数执行前)就创建唯一实例,无论后续是否使用。利用全局静态变量的初始化特性,天然保证实例唯一性。
2. 完整代码实现
cpp
#include <iostream>
#include <string>
using namespace std;
// 单例模式 - 饿汉模式
class Singleton
{
public:
// 全局访问点:返回唯一实例的指针/引用
static Singleton* GetInstance()
{
return &m_instance;
}
// 示例:测试成员函数
void PrintInfo()
{
cout << "Singleton Instance Address: " << this << endl;
}
private:
// 1. 私有化构造函数:禁止外部创建对象
Singleton()
{
cout << "饿汉模式:Singleton 构造函数调用" << endl;
}
// 2. 禁用拷贝构造和赋值运算符(C++98/C++11 两种写法)
// C++98:私有 + 只声明不定义
// Singleton(const Singleton&);
// Singleton& operator=(const Singleton&);
// C++11:显式删除(推荐)
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 3. 静态成员变量:程序启动时初始化,保证唯一
static Singleton m_instance;
};
// 全局静态成员初始化(main函数前执行)
Singleton Singleton::m_instance;
// 测试代码
int main()
{
// 获取两个实例指针,验证地址一致
Singleton* ptr1 = Singleton::GetInstance();
Singleton* ptr2 = Singleton::GetInstance();
ptr1->PrintInfo(); // 输出:Singleton Instance Address: 0xXXXXXXX
ptr2->PrintInfo(); // 输出:与ptr1完全相同
// 验证拷贝/赋值被禁用(编译报错)
// Singleton obj = *ptr1; // 拷贝构造被删除
// *ptr2 = *ptr1; // 赋值运算符被删除
return 0;
}
3. 优缺点分析
| 优点 | 缺点 |
|---|---|
| 实现简单,无线程安全问题(静态变量初始化由编译器保证原子性) | 程序启动时即初始化,若实例未被使用则浪费内存 |
| 多线程高并发场景下响应快(无需加锁) | 多个单例类的初始化顺序无法控制 |
| 无内存泄漏风险(静态变量由系统自动销毁) | 若实例构造耗时(如加载大文件),会导致程序启动慢 |
4. 适用场景
- 单例对象构造耗时短、占用资源少;
- 程序运行期间必然会使用该实例;
- 多线程高并发场景(避免懒汉模式的加锁开销)。
三、懒汉模式:延迟加载(按需初始化)
1. 核心思路
"懒汉" 即 "懒加载"------第一次调用 GetInstance 时才创建实例 ,程序启动时不初始化,避免资源浪费。核心挑战是解决多线程下的线程安全问题。
2. 线程安全的完整实现(Double-Check 优化)
cpp
#include <iostream>
#include <string>
#include <mutex>
#include <thread>
using namespace std;
// 单例模式 - 懒汉模式(线程安全版)
class Singleton
{
public:
// 全局访问点:Double-Check 加锁,保证效率+线程安全
static Singleton* GetInstance()
{
// 第一次检查:避免每次调用都加锁(提升效率)
if (nullptr == m_pInstance)
{
m_mtx.lock(); // 加锁:保证多线程下只有一个线程创建实例
// 第二次检查:防止多个线程等待锁后重复创建
if (nullptr == m_pInstance)
{
m_pInstance = new Singleton();
cout << "懒汉模式:Singleton 构造函数调用" << endl;
}
m_mtx.unlock();
}
return m_pInstance;
}
// 内嵌垃圾回收类:解决单例对象的内存泄漏问题
class CGarbo
{
public:
~CGarbo()
{
// 程序结束时自动调用析构,释放单例对象
if (Singleton::m_pInstance)
{
delete Singleton::m_pInstance;
Singleton::m_pInstance = nullptr;
cout << "懒汉模式:Singleton 析构函数调用" << endl;
}
}
};
// 静态垃圾回收对象:程序结束时系统自动销毁
static CGarbo Garbo;
// 示例:测试成员函数
void PrintInfo()
{
cout << "Singleton Instance Address: " << this << endl;
}
private:
// 1. 私有化构造函数
Singleton() {}
// 2. 禁用拷贝构造和赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 3. 静态成员变量
static Singleton* m_pInstance; // 单例对象指针(初始化为nullptr)
static mutex m_mtx; // 互斥锁:保证线程安全
};
// 静态成员初始化
Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Singleton::Garbo;
mutex Singleton::m_mtx;
// 多线程测试函数
void ThreadFunc()
{
Singleton* ptr = Singleton::GetInstance();
ptr->PrintInfo();
}
// 测试代码
int main() {
// 创建多个线程,验证实例唯一性
thread t1(ThreadFunc);
thread t2(ThreadFunc);
thread t3(ThreadFunc);
t1.join();
t2.join();
t3.join();
return 0;
}
3. 核心设计解析
(1)Double-Check 加锁机制
- 第一次检查(无锁):若实例已创建,直接返回,避免每次调用都加锁(提升高并发场景下的效率);
- 加锁:保证多线程下只有一个线程进入实例创建逻辑;
- 第二次检查(加锁后):防止多个线程等待锁后重复创建实例(比如线程 A 创建实例前,线程 B 已等待锁,A 创建完成后 B 若不检查会再次创建)。
(2)内嵌垃圾回收类(CGarbo)
懒汉模式使用 new 创建堆对象,若手动调用 delete 易遗漏,导致内存泄漏。内嵌垃圾回收类的核心逻辑:
CGarbo是静态成员对象,程序结束时系统会自动调用其析构函数;- 析构函数中删除单例对象,保证资源正常释放。
4. 优缺点分析
| 优点 | 缺点 |
|---|---|
| 延迟加载:仅在第一次使用时初始化,节省内存 | 实现复杂,需处理线程安全和内存泄漏问题 |
| 程序启动快,无初始化负载 | Double-Check 加锁存在微小的性能开销(远低于每次加锁) |
| 多个单例类的初始化顺序可自由控制 | C++11 前可能存在指令重排导致的线程安全隐患(需 volatile 优化) |
5. 适用场景
- 单例对象构造耗时久、占用资源多(如加载插件、初始化网络连接);
- 程序运行期间可能不使用该实例;
- 对程序启动速度要求高。
四、饿汉 vs 懒汉:核心对比
| 特性 | 饿汉模式 | 懒汉模式 |
|---|---|---|
| 初始化时机 | 程序启动时(main 前) | 第一次调用 GetInstance 时 |
| 线程安全 | 天然安全(编译器保证) | 需加锁(Double-Check) |
| 内存占用 | 启动即占用,可能浪费 | 按需占用,更节省 |
| 程序启动速度 | 可能变慢(构造耗时) | 快(无初始化负载) |
| 实现复杂度 | 简单(几行代码) | 复杂(加锁 + 垃圾回收) |
| 初始化顺序 | 无法控制 | 可自由控制 |
五、进阶优化:C++11 简化版懒汉模式
C++11 规定:局部静态变量的初始化是线程安全的,可利用这一特性实现极简的懒汉模式:
cpp
class Singleton
{
public:
static Singleton& GetInstance()
{
// 局部静态变量:第一次调用时初始化,线程安全
static Singleton instance;
return instance;
}
// 禁用拷贝构造和赋值运算符
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() {} // 私有化构造函数
};
// 测试调用
Singleton& instance = Singleton::GetInstance();
该方案无需加锁、无需垃圾回收类,兼顾简洁性和线程安全,是 C++11 及以上版本的首选懒汉模式实现。
六、注意事项
- 禁用拷贝 / 赋值 :无论哪种模式,必须禁用拷贝构造和赋值运算符,否则可能通过 **
Singleton obj = *GetInstance();**生成多实例; - 线程安全:懒汉模式务必处理线程安全问题,避免多线程下创建多个实例;
- 内存泄漏:懒汉模式需保证堆对象正常释放(推荐内嵌垃圾回收类或 C++11 局部静态变量);
- 单例滥用:单例模式会增加代码耦合性,非必要场景(如可通过参数传递的对象)避免使用。
总结
- 单例模式的核心是构造函数私有化 + 禁用拷贝 + 静态全局访问点,保证实例唯一性;
- 饿汉模式简单、线程安全,但可能浪费资源,适用于轻量级实例;
- 懒汉模式延迟加载、节省资源,需处理线程安全和内存泄漏,C++11 可通过局部静态变量简化实现;
- 选择哪种模式取决于实例的构造开销、使用频率及程序启动性能要求。