目录
讲设计模式之前,我们先了解一下设计模式三要素:
设计模式三要素
1️⃣单一职责原则
就是对一个类而言,应该只提供一种功能,尽量使类的功能单一化,避免类的臃肿。
2️⃣开放封闭原则
对于扩展是开放的,对于修改是封闭的。对于已经封装好的类应该避免对其内部进行修改, 如果要添加其他功能,应在类外部再进行其他类的定义和实现。
3️⃣依赖倒转原则
高层模块不应该依赖低层模块,两个都应该依赖抽象
抽象不应该依赖细节,细节应该依赖抽象
高层:理解为上层应用,就是业务层的实现
低层:理解为低层接口,封装好的API、动态库等
抽象:指的是抽象类或者接口,在c++中没有接口,只有抽象类
如果高层直接依赖低层,当低层代码或接口变化,会导致高层也需要对应的修改,无法实现对高层代码的复用。在高层和低层之间增加一个抽象类,高层通过抽象类的方法来进行调用低层方法的实现。
意味着要对细节进行封装,就是将其放入一个抽象类中。实质上就是增加了一个代理人,想要什么就通过代理人获取。
设计模式分为三大类,分别是创建型、结构型、行为型
📚什么是单例模式?
单例模式是一种创建型设计模式,它的核心目的是保证一个类在整个应用程序生命周期中只有一个实例 ,并提供一个全局访问点来获取这个实例。这种模式在需要严格控制资源访问或管理全局状态的场景中特别有用。稍微不注意会出现隐藏问题。
❇️核心要素
单例模式的核心思想不仅仅是"一个类只能有一个实例",更重要的是它解决了多个客户端共享同一个资源时的协调问题。想象一下,如果系统中的多个组件都需要访问同一个配置管理器或数据库连接池,但没有统一的访问机制,就会导致资源冲突、状态不一致等问题。
单例模式的价值体现:
- 资源管理:对于重要的资源(如数据库连接、文件句柄等),单例可以避免重复创建和销毁的开销
- 状态一致性:确保所有客户端访问的是同一个状态,避免数据不一致
- 访问控制:提供统一的访问入口,便于实施访问控制和监控
举个例子:
比如小区中的快递柜:小区里有多个住户(对应 "系统中的多个组件"),大家都需要使用同一个快递柜(对应 "配置管理器 / 数据库连接池")来存放或取件,但是如果没有统一的管理机制,就可能会出现以下问题:
- 住户 A 刚把快递放进某个柜子,还没来得及关门,住户 B 没注意,直接在A的柜子里放自己的快递,导致两人的快递混在一起、甚至丢失(资源冲突)。
- 快递员 C 不知道某个柜子已经被住户 D 占用,重复扫码打开该柜子放新快递,造成 D 的快递被替换,双方各执一词(状态不一致)。
- 有些住户用完后不关门,导致柜子长期被 "霸占",其他有需要的住户找不到空柜子,而快递柜实际还有很多柜子因未被正确释放而无法使用(资源浪费 + 访问阻塞)。
而快递柜的统一管理系统(对应 "单例模式的统一访问机制"),会给每个格子分配唯一编号,每次只允许一个用户操作一个格子,操作完成后自动锁定并标记状态,所有住户和快递员都通过这个统一系统使用快递柜,就不会出现混乱。
实现单例模式需满足的条件:
- 唯一实例 :单例类只能有一个实例,禁止外部通过new、拷贝构造等方式创建新的实例。
- 全局访问 :提供一个全局静态方法 (常见的就比如
getInstance())提供给外部获取实例。 - 自主管理生命周期 :实例的创建和销毁由类自身控制,避免外部干预。
*️⃣单例模式常用的实现方式
⏩饿汉模式
顾名思义,饿得受不了了,一看见食物就想吃------也就是在程序启动时(main函数执行之前),就创建了实例,可以确保线程安全,但是可能会浪费内存。
cpp
class Singleton {
private:
// 1. 保护构造函数:禁止外部创建实例
Singleton() = default;
// 2. 禁用拷贝构造和赋值运算符(C++11起)
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 3. 静态成员变量:存储唯一实例
static Singleton instance;
public:
// 4. 全局访问点
static Singleton& getInstance() {
return instance;
}
};
// 类外初始化静态成员(程序启动时创建)
Singleton Singleton::instance;
优点:
- 实现简单,天然线程安全 (C++ 标准规定全局变量初始化在主线程执行)。
缺点:
- 实例在程序启动时创建,如果单例构造函数耗时或者占用资源多,会拖慢程序启动速度。
- 无法在创建实例时传递参数(全局变量初始化不支持动态参数)。
⏩懒汉模式
顾名思义,太懒了,看到想吃的食物也懒得吃,除非迫不得已------除非调用单例类的静态全局访问函数,才会创建对象,否则不会。这样虽然节省了资源,但是需要处理线程安全问题。
线程安全问题在于:多线程同时判断单例对象是否为空时,可能同时进入实例化代码块,导致创建多个实例 。解决这一问题的核心思路是通过同步机制控制实例化过程,确保同一时刻只有一个线程能执行实例化逻辑 。
⚠️基础版(线程不安全)
cpp
class Singleton {
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 静态指针(延迟初始化)
static Singleton* instance;
public:
static Singleton& getInstance() {
if (instance == nullptr) {
instance = new Singleton(); // 第一次调用时创建
}
return *instance;
}
// 可选:手动释放实例(不推荐,易引发析构问题)
static void destroyInstance() {
if (instance != nullptr) {
delete instance;
instance = nullptr;
}
}
};
// 初始化为nullptr
Singleton* Singleton::instance = nullptr;
问题 :多线程环境下,if (instance == nullptr)可能被多个线程同时通过,导致创建多个实例(违反了单例唯一性)。
⁉️那该怎么解决?
1️⃣第一种:C++11引入了"魔术静态"------局部静态变量的初始化在多线程环境下是线程安全的,可简化懒汉式实现。
但是C++11之前,局部静态变量的初始化不是线程安全的,需要手动加锁。
cpp
class Singleton {
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
// 局部静态变量:第一次调用时初始化,线程安全(C++11及以上)
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
};
优点:
- 线程安全(依赖 C++11 标准,主流编译器均支持)。
- 延迟初始化,不浪费资源。
- 自动析构(程序结束时由系统销毁,无需手动释放)。
缺点:
- 析构顺序不确定(如果单例依赖其他全局对象,可能会出现析构顺序问题)。
2️⃣第二种:简单粗暴,直接上锁(std::mutex),它可以确保同一时刻只有一个线程能执行实例化逻辑,是最直接的线程安全实现。
cpp
class Singleton {
private:
// 私有静态实例(延迟初始化)
static Singleton* instance;
// 全局互斥锁
static std::mutex mtx;
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton* getInstance() {
// 加锁:确保同一时刻只有一个线程进入
std::lock_guard<std::mutex> lock(mtx);
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};
// 初始化静态成员
LazySingleton* LazySingleton::instance = nullptr;
std::mutex LazySingleton::mtx;
std::lock_guard 会在构造时锁定互斥锁 mtx,析构时自动释放,确保 getInstance() 方法在多线程下的原子性 ------ 任何时刻只有一个线程能执行 if (instance == nullptr) 及后续实例化逻辑。
但是,锁的粒度太大。即便使实例已经创建(创建后被调用只需直接返回),每次调用 getInstance() 仍会触发锁竞争,高并发场景下性能损耗明显。
所以需要对它进行优化:
cpp
static Singleton* getInstance() {
// 第一次检查:如果实例已存在,直接返回(无锁,提升效率)
Singleton* temp = instance.load(std::memory_order_acquire);
if (temp == nullptr) {
// 加锁:仅当实例未创建时,才进入同步逻辑
std::lock_guard<std::mutex> lock(mtx);
// 第二次检查:防止多线程同时通过第一次检查后,重复创建实例
temp = instance.load(std::memory_order_relaxed);
if (temp == nullptr) {
temp = new Singleton();
// 用 release 语义存储实例,确保其他线程能看到完整初始化的对象
instance.store(temp, std::memory_order_release);
}
}
return temp;
}
核心要点:
- 两次判断:
-
- 第一次判断(无锁):如果实例已创建,直接返回,避免进入锁逻辑,减少性能损耗。
- 第二次判断(加锁后):防止多个线程同时通过第一次判断,导致锁内重复创建实例。
- std::atomic****与内存序:
-
std::atomic确保instance指针的读写操作在多线程下的原子性和可见性。memory_order_acquire和memory_order_release用于防止指令重排序:避免 "对象未完全初始化就被其他线程读取" 的问题(例如,new Singleton()可能被拆分为 "分配内存→指针赋值→初始化对象",若重排序,其他线程可能拿到未初始化的对象)。
优点:仅在实例没有创建时加锁,后续调用不用加锁,性能接近无锁的饿汉模式,适合高并发场景。
❗️单例模式需注意的点
❌拷贝构造和赋值运算符未禁用
问题:若未显式禁用拷贝构造和赋值运算符,外部可能通过拷贝创建新实例
解决 :C++11 起用= delete显式禁用:
cpp
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
⭕️继承与单例的冲突
一般来说单例类是不建议继承的,因为无法保证子类唯一实例
但是,如果还是想要子类拥有单例的属性,那么就使用奇异递归模板(CRTP)
cpp
template<typename T>
class Singleton {
protected:
Singleton() = default;
public:
static T& getInstance() {
static T instance;
return instance;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
class MySingleton : public Singleton<MySingleton> {
private:
// 必须将基类设为友元,才能访问protected构造函数
friend class Singleton<MySingleton>;
MySingleton() = default;
public:
void doSomething() {
// 业务逻辑
}
};
// 使用方式
MySingleton::getInstance().doSomething();
⚠️易误解的点
单例并非 "只能有一个实例"
严格来说,单例模式是 "在特定作用域内唯一"(通常是进程内)。在分布式系统或多进程场景中,每个进程可拥有自己的单例实例。
单例模式不适合频繁创建销毁的场景
单例的实例一旦创建,通常会存活至程序结束。若某对象需要频繁创建销毁(如临时缓存),使用单例反而会浪费资源。
📝总结
单例模式看似简单,实则涉及多线程、内存管理、生命周期等多个细节。在 C++ 中,推荐优先使用C++11 魔术静态的懒汉式(线程安全、自动析构、实现简洁),并注意禁用拷贝构造和赋值运算符,避免破坏唯一性。
最后贴一个实现单例模式的代码
cpp
#include <memory>
#include <mutex>
#include <iostream>
using namespace std;
template <typename T>
class Singleton {
protected:
Singleton() = default;
Singleton(const Singleton<T>&) = delete;
Singleton& operator=(const Singleton<T>& st) = delete;
static std::shared_ptr<T> _instance;
public:
static std::shared_ptr<T> GetInstance() {
static std::once_flag s_flag;
std::call_once(s_flag, [&]() {
_instance = shared_ptr<T>(new T);
});
return _instance;
}
void PrintAddress() {
std::cout << _instance.get() << endl;
}
~Singleton() {
std::cout << "this is singleton destruct" << std::endl;
}
};
template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;
谢谢大家!