当前位置: 首页 > news >正文

设计模式学习[19]---单例模式(饿汉式/懒汉式)

文章目录

  • 前言
    • 1.引例
    • 2.单例在多线程情况
    • 3.饿汉式与懒汉式
    • 4.内存释放问题
  • 总结

前言

我们在游戏里面使用的游戏配置,还有软件开发中打日志的类,从合理性来说,一般只会创建一个实例,给所有用户去使用。

比如我游戏中对图像精细程度,窗口比例等进行调整,这种一般只会设置一次,适配当前电脑。不可能说,我换个存档,这个显示设置就全变了吧?

还有就是打日志,其实打日志就是往文件或者控制台输出一段文本,用来提示或者警告的作用,方便程序后续出现异常定位问题,那这种情况如果我每次都创建日志对象,输出日志,再去销毁这个对象,在庞大的程序里面,这种就非常浪费性能,显然是不可取。

于是,我们期望部分类在整个软件的生命周期中,有且只有一个对象,于是就有了单例模式

1.引例

我们现在对于游戏配置来举个例子,由于是单例,所以我们并不希望GameConfig这个类在外部通过默认构造函数,拷贝构造函数,赋值构造函数等方式进行构造。同时它也不可以被外界随意释放。

下面是一个基本的单例模式的样板

#include <iostream>class GameConfig
{
private:GameConfig() {};GameConfig(const GameConfig& tmpobj);GameConfig& operator=(const GameConfig& tmpobj);~GameConfig() {};public:static GameConfig* getInstance(){if (m_instance == nullptr){printf("创建m_instance\n");m_instance = new GameConfig();}printf("返回m_instance=%p\n", m_instance);return m_instance;}private:static GameConfig* m_instance;
};GameConfig* GameConfig::m_instance = nullptr;//类外定义静态成员int main()
{GameConfig* g_gc = GameConfig::getInstance();//GameConfig* g_gc2 = GameConfig::getInstance(); //创建不同指针获取实例,指向的是同一个对象}

2.单例在多线程情况

对于上面的例子,如果是在多线程的情况,我们在14行,if (m_instance == nullptr),可能会出现下面的情况:
A线程在判断完m_instance为空之后,开始创建对象,但是还没创建完,此时B线程也进入到m_instance == nullptr的判断,这时候也开始创建对象。这就会导致单例失效,在程序的某些地方可能会导致难以排查的错误。

那么对于这种情况,最先想到的处理方式肯定就是加锁。
我们在类外定义一个锁std::mutex GameConfig::my_mutex,并在类内声明,同时在获取单例对象的时候,加锁,获取完后自动释放

初步改版后,代码如下:

static GameConfig* getInstance(){std::lock_guard<std::mutex>gcguard(my_mutex);if (m_instance == nullptr){printf("创建m_instance\n");m_instance = new GameConfig();}printf("返回m_instance=%p\n", m_instance);return m_instance;}

这种情况,看似是解决了多线程的冲突,但是我们考虑一下,如果每次获取这个对象都需要加锁解锁,如果这个获取单例对象的函数是一个热点函数,那就需要反复调用这个函数,那么频繁的加锁解锁对性能的影响是非常大的,所以直接加锁是一种比较暴力的方式,考虑还欠缺。

那有没有一种方式能够优化一下呢?
我们加锁其实只针对m_instance == nullptr这种情况有意义,因为我们加锁的是为了在初始化的时候,确保在创建对象的时候,其他线程不会创建新的GameConfig对象,始终确保这个对象只有一份,达到单例的标准。
那么,在对象创建完之后,这个对象其实就只是一个只读对象,在多线程中,对一个只读对象的访问进行加锁不但代价很大,且没有意义。所以我们考虑用一个双重锁定机制,基于这种机制的getInstance成员函数代码实现如下:

static GameConfig* getInstance(){if (m_instance == nullptr){//加锁std::lock_guard<std::mutex>gcguard(my_mutex);if (m_instance == nullptr){printf("创建m_instance\n");m_instance = new GameConfig();}}printf("返回m_instance=%p\n", m_instance);return m_instance;}

这里双重锁定机制在第一个判断时,并没有加锁,而是在第二给if判断才加锁。

(1)首先,如果条件if(m_instance!=nullptr)成立,则肯定代表m_instance已经被new过了
(2)如果条件if(m_instance==nullptr)成立,不代表m_instance一定没被new过,这个在多线程一开始就说过了。

那么,在m_instance可能被new过(也可能没被new过)的情况,再去加锁。加锁后,只要这个锁能锁住,就再次判断条件if(m_instance==nullptr),如果这个条件依然满足,那么肯定表示这个单例对象还没有被初始化,这时候就可以使用new来初始化。

在new完对象之后,我们后面正常的调用,因为if(m_instance==nullptr)始终不成立,直接返回对象。因为用了双重判断的方式,所以后面返回对象前,并没有进行加锁解锁操作,这就提高了执行getInstance的效率。

难道双重判断就无敌了吗?其实也不尽然。即使加了双重锁定机制,这里也还存在潜在问题—内存访问重新排序(重新排列编译器产生的汇编指令)导致双重锁定失效的问题。
这里就走远了,具体的可以互联网上查询一下研究一下。

总之,一个好的解决多线程创建 GameConfig类对象问题的方法是:在main主函数(程序执行入口)中,在创建任何其他线程之前,先执行一次“GameConfig::getInstance();”来把这个单独的 GameConfig 类对象创建出来。这样,后续再对 GameConfig类的 getInstance 成员函数进行调用时就相当于只读取m_instance 成员变量,对getInstance 成员函数的调用就不再需要加锁了。

3.饿汉式与懒汉式

我们上面的这种实现单例的实现方式是懒汉式,因为对象的创建在我们要去获取的时候才开始第一次创建。而饿汉式就是在一开始就把对象创建好。

为什么叫这两个名字,emm,第一个就是一开始不创建对象,要用到来才创建,属于拖延式的。而第二种一开始就创建,就很饥渴,所以就叫饿汉式。emm,大体就是这么一回事~!

我们懒汉式代码上面已经编写了,下面是饿汉式的代码

class GameConfig
{
private:GameConfig() {};GameConfig(const GameConfig& tmpobj);GameConfig& operator=(const GameConfig& tmpobj);~GameConfig() {};public:static GameConfig* getInstance(){return m_instance;}private:static GameConfig* m_instance;
};GameConfig* GameConfig::m_instance = new GameConfig();

饿汉式的代码就不存在多线程的问题,因为一开始就把对象定义好了,getInstance函数只负责返回一个对象。

4.内存释放问题

我们的单例都是通过new的方式进行创建,所以释放的话,正常都需要手动调用一次delete,乍一看号线也挺OK的,比如我们在GameConfig这个类里面写一个成员函数

public:static void freeInstance(){if(m_instance!=nullptr){delete GameConfig::m_instance;GameConfig::m_instance = nullptr;}}

这个函数需要去手动调用一下,如果忘记了,就内存泄漏了,所以并不保险。最好的方案还是让他自动释放,所以不放看一下下面这个方案。

通过在GameConfig中放一个Garbo类(嵌套类),将单例对象的生命周期和Garbo捆绑到一起

class Garbo
{
public:~Garbo(){if (GameConfig::m_instance != nullptr){delete GameConfig::m_instance;GameConfig::m_instance = nullptr;}}
};

如果是饿汉式模式

在类GameConfig定义中,增加一个private修饰的静态成员变量:

private:static Garbo garboobj;

在类外,cpp源文件的开头位置,对上述静态成员进行定义:

GameConfig::Garbo GameConfig::garboobj;

在释放garboobj的时候,会自动调用其析构函数,同时将m_instance也一同删除并释放内存。

如果是懒汉式

则意味着如果不调用getInstance成员函数,则单件类对象不会被new出来,自然也就不需要释放。具体的实现代码相对简单,只需要在getInstance 成员函数中的new语句行下面增加一个局部静态变量定义即可:

static GameConfig* getInstance()
{if (m_instance == nullptr){printf("创建m_instance\n");m_instance = new GameConfig();static Garbo garboobj;}printf("返回m_instance=%p\n", m_instance);return m_instance;
}

经过上述步骤后,如果程序调用过getInstancegetInstancegetInstance为单件类分配了内存,自然也就相当于garboobjgarboobjgarboobj局部静态变量被构造出来了,该局部静态变量所分配的内存会在程序执行结束前由操作系统回收,该回收动作会导致garboobj所属类Garbo析构函数的执行,在该析构函数中,正好释放单件类对象。

总结

单例模式是常用的一种设计模式,这里对于饿汉式和懒汉式的选择,其实我更倾向于饿汉式,这样就避免了多线程问题。本篇博客还介绍了一种垃圾回收的方式,通过类对象的析构来自动回收,这样避免了忘记释放导致内存泄漏的问题。

最后再贴一下UML类图
在这里插入图片描述

http://www.dtcms.com/a/393637.html

相关文章:

  • 基于哈希表与差分前缀和解决撒狗粮问题
  • 基于多设计模式的状态扭转设计:策略模式与责任链模式的实战应用
  • 残差分析:数据驱动下线性模型的“体检师”与优化指南
  • gorm速成
  • 模型和策略:风控体系的“左右手”之争
  • Keil5 5.38版本在使用STLINK时闪退
  • win11 安装 WSL2 Ubuntu 并支持远程 SSH 登录
  • 基分析积分法则
  • 【Linux】网络——HTTP协议,HTTPS加密
  • HarmonyOS动画:属性动画、显示动画、转场动画
  • Redis 持久化机制详解:RDB 与 AOF 原理与实践
  • 【嵌入式协议外设篇】-8×8 点阵屏
  • 【C++:STL】深入详解string类(一):从读文档开始
  • 电商项目实战总结
  • 22.元类、静态鸭子类型、抽象基类
  • 【论文速递】2025年第21周(May-18-24)(Robotics/Embodied AI/LLM)
  • Android 自定义电池组件(BatteryView)
  • 基于 Stripe/Metering 的用量计费:从 SLO 指标到账单流水
  • 如何解决 pip install 安装报错 ModuleNotFoundError: No module named ‘fastapi’ 问题
  • 论文阅读——隧道中毫米波MIMO信道特性的实验研究
  • The Library: 1靶场渗透
  • 23种设计模式之【装饰器模式】-核心原理与 Java实践
  • 动态规划中的背包问题:0/1 背包与完全背包的核心解析
  • PHP应用-组件框架前端模版渲染三方插件富文本编辑器CVE审计(2024小迪安全DAY30笔记)
  • uniapp 如何判断发的请求是网络异常uni.request
  • 学习:uniapp全栈微信小程序vue3后台 (25)
  • 23种设计模式之【原型模式】-核心原理与 Java实践
  • Netty 重放解码器ReplayingDecoder揭秘:重写轻量异常机制 和 ConstantPool
  • getgeo 生物信息 R语言 表型信息表”“样本信息表”或“临床信息表 phenodata phenotype data
  • OceanBase备租户创建(二):通过BACKUP DATABASE PLUS ARCHIVELOG