【多线程-从零开始-柒】代码案例1—单例模式

news2024/9/29 1:31:01
  • 单例模式:是一种设计模式
    • 设计模式,类似于“棋谱”,就是固定套路,针对一些特定的场景,给出一些比较好的解决方法
    • 只要按照设计模式来写代码,就可以保证代码不会太差,保证代码的下限

设计模式

设计模式

  • 设计模式并非只有 23 种
  • 23 这个数字是有三个大佬(GOF),写了一本设计模式的书,这本书中谈到了 23 种设计模式
  • 设计模式也适合变成语言相关的,有些设计模式,是在给语法填坑
  • 有的语言本身语法设计的更好,不太依赖设计模式
  • 这 23 种设计模式是针对 C# 这个语言来谈的,比较适用于 C++,Java、C#
  • 校招中,掌握 4~5 个就好了
  • 设计模式适合具有一定的编程经验之后再学习,因为缺少经验,难以理解人家为什么这样写
  • 编程这个圈子中,“灵活”是贬义词(易出 bug),“死板”才是褒义词(稳健)
  • 设计模式就是针对编写代码过程中的“软性约束”(不是强制的,选择性遵守,但遵守当然有好处)

框架

  • 针对一些特定的场景问题,大佬们把基本的代码已经写好了,大部分逻辑也都写好了,留出来一些空位,让你在空位上填充一些自定制的逻辑
  • 框架就是针对编写代码过程中的“硬性约束”
  • Java 圈子中,特别讲究框架

单例模式

  • 开发者,希望有的类在一个进程中,不应该存在多个实例(对象)
  • 此时,就可以使用单例模式(单个实例/单个对象),只有唯一实例
    • 比如在 JDBC
      • JDBC => DataSourse 数据源描述数据库在哪
      • 一般来说,一个程序中,只有一个数据库,对应的 mysql 服务器只有一份,此时 DataSourse 这个类没有必要创建出多个类
      • 此时就可以使用单例模式,描述 DataSourse,避免不小心创建出了多个实例
    • 比如广告系统
      • 广告数据都是要加载在内存中的,这里的查询就是直接查内存 hash 表,避免直接插数据库(查数据库太慢)
      • 所以就在代码中专门搞了个类,管理这些数据,此时这个类也就是案例模式
      • 这个类,一个实例就是几百G 内存空间

前提是“一个进程中”,如果有多个进程,自然每个进程中都可以有一个实例

  • Java 中,单例模式的实现有很多种写法,这里我们介绍两种最主流的写法:饿汉模式懒汉模式

理解

在你们家,每次吃完饭后,如果是你的妈妈洗碗,那一般都是吃完后立即就把碗洗了
但如果是你洗碗,就会把碗先泡一会再洗,但这样容易忘记,一拖,就拖到了下顿使用碗的时候才会洗

你妈妈这种洗碗习惯就相当于是“饿汉模式
你这种洗碗模式就相当于是“懒汉模式

饿汉模式

“饿”的意思是“迫切”,在类被加载的时候,就会创建出这个单例的实例

  1. Singleton 类中,成员变量 instance 要用 static 修饰,这样 instance 就变成了“类成员”
    类成员初始化,就是在 Singleton 这个类被加载的时候(近似于程序启动的时候)
class Singleton {  
    //static修饰,instance 成员变量就是“类成员”  
    private static Singleton instance = new Singleton();  
}
  • 程序启动,类就加载了,这个类一加载,这里的初始化就进行了,于是这里的实例就创建了
  • 这就叫实例创建的非常迫切,就叫“饿汉模式”

  1. 不过有了实例还不够,还需要外面能对实例进行使用,我们再创建一个方法
class Singleton {  
    //static修饰,instance 成员变量就是“类成员”  
    private static Singleton instance = new Singleton();  
	
	//获取实例对象
	public static Singleton getInstance() {  
        return instance;  
    }
}
  • 之后我们需要使用实例的时候,只需要调用这个 getInstance 方法就好了
  • 现在只要我们不在其他代码中,new 这个类,每次需要使用都通过 getInstance 方法来调用到这个实例,此时这个类就是单例的了

  • 但这个设定其实不科学,凭什么你说别人都不去 new 这个类,这就相当于是一个君子协议,口头约定一下。但万一不遵守呢?
  • 所以“ 只要我们不在其他代码中,new 这个类 ”这个才是单例模式中主要需要解决的问题,防止别人不小心 new 了对象

类的使用者的想法都很简单,“用就对了”,但类的设计者需要考虑的事就多了

  • 如何避免这个的实例不小心被 new 了一下
  1. 单例模式最关键一步:将类的构造方法设为私有
class Singleton {  
    //static修饰,instance 成员变量就是“类成员”  
    private static Singleton instance = new Singleton();  
	
	//获取实例对象
    public static Singleton getInstance() {  
        return instance;  
    }  
    
    //最关键的一步
    private Singleton() {}  
}
  • 这就意味着在类的外面,就无法调用构造方法,也就无法创建实例了image.png

单例模式只能避免别人的“失误”,但无法应对别人的“故意攻击”:

  • 你不是 private 吗,我通过反射拿到构造方法,通过反射 API 来调用,不就能 new 出实例了吗
  • 除了反射,还可以通过“序列化反序列化”打破上述单例模式

懒汉模式

计算机中,“懒”是褒义词。谈到“懒”,效率会更高,是高效的代名词
在这里,推迟了创建实例的时机,实例会在第一次使用的时候才会创建


经常有这种情况:
中午的时候,需要使用 4 个碗
晚上的时候,只需要使用 2 个碗,此时只需要洗两个碗就可以了

  • 洗两个碗,比洗四个碗更高效
  • 能不搞就不搞,很多时候就可以把这部分开销就省下了
    这里的优劣只是在计算机中,不是在生活中!!!

比如,有一个编辑器,打开一个非常大的文本文档,有两种方式:

  • 一启动,就把所有的文本内容都读取到内存中,然后再显示到界面上[饿汉]
  • 启动之后,只加载一小部分数据(一个屏幕能显示的最大数据),随着用户进行翻页操作,再按需地加载剩下的内容[懒汉]
    毋庸置疑,我们更青睐于“懒汉模式”
class SingletonLazy {  
	//先把实例的引用设为 null,不着急创建实例
    private static SingletonLazy instance = null;  
  
    public static SingletonLazy getInstance() {  
        if(instance == null) {  
            instance = new SingletonLazy();  
        }        
        return instance;  
    }
    
    private SingletonLazy() {}
}
  • 先把实例的引用设为 null,不着急创建实例
  • 当首次调用 getInstance,由于此时引用为 null,就会进入 if 分支,从而创建实例
  • 后续再重复调用 getInstance,结果都不会创建实例,直接返回
  • 还是要将构造函数设为 private,避免被不小心 new

是否线程安全

上述写的“饿汉“和“懒汉“单例模式代码,是否是线程安全的?(如果是多线程环境下,调用 getInstance,是否会有问题呢?)

  1. 饿汉模式
public static Singleton getInstance() {  
        return instance;  
    } 

无线程安全问题

  • 这里的 return instance 只是一个“读操作”,没有涉及到“多个线程对变量进行修改”
  1. 懒汉模式
public static SingletonLazy getInstance() {  
        if(instance == null) {  
            instance = new SingletonLazy();  
        }        
        return instance;  
    }

有线程安全问题

  • 这里的赋值操作就涉及到“修改”了,还有一个“判定”操作结合在一起,这样就非常容易产生“线程安全”问题了

对于 判定+赋值 操作,产生的线程安全问题:

if(instance == null) {  
    instance = new SingletonLazy();      
} 
调度顺序线程t1t2
if (istance == null)
if (instance == null)
由于 instance 仍然为 null,所以 t2
会认为这个条件也是满足的
instance = new SingletonLazy();
创建实例
instance = new SingletonLazy();
由于 t2 刚刚完成了条件判断,
所以 t2 也会执行创建实例的逻辑
  • 上述逻辑中,就创建了两个实例
  • 第二次创建,覆盖了 instance 的值,使得第一次创建的实例没有引用指向,很快就会被垃圾回收机制给消除掉
  • 但即使如此,仍然认为上述代码是存在 bug
    • 因为实际上,构造方法内部可能会执行很多的逻辑

“先判定,再修改”,这种代码模式,是属于典型的线程不安全代码,因为判定和修改之间可能涉及到线程的切换

如何解决线程安全问题

1. 线程切换导致的线程安全

通过加锁,来解决问题

  • 出现线程安全问题,是因为 if 判定和 new 操作之间出现了线程切换,出现了逻辑上的穿插
  • 所以需要锁,把 ifnew 打包成一个整体就可以了
public static SingletonLazy getInstance() {  
    synchronized (locker) {  
        if (instance == null) {  
            instance = new SingletonLazy();  
        }    
    }    
    return instance;  
}
  • 这样,if 判定和 new 操作就是一个“原子”了
  • return 加不加到锁里面无所谓
    • 此处的矛盾是 ifnew 中间出现线程切换,引起逻辑错误,而后面的 return 不会受到影响
    • 无论 return 是在本线程还是其他线程,此处的 return 的值都是 instance 内存中的最新值

2. 加锁导致的性能下降

加锁之后,确实解决了线程安全问题,到那时加锁却可能会带来阻塞
如果上述代码,已经 new 完了对象,if 分支再也进不去了,后续的代码,都是单纯的“读操作”,此时 getInstance 不加锁也是线程安全的
这样就没有必要加锁了

而当前的代码写法,虽然没有线程安全问题了(instance new 出来之后,就都是读操作了),但只要调用了 getInstance,就都会触发加锁操作,此时就会因为加锁,而产生阻塞,啥时候能恢复执行,中间可能是“沧海桑田”,影响性能

因此,针对这个问题,还需要进行进一步改进

  • 通过条件判断,在需要加锁的时候才加锁,不需要的时候就不加
public static SingletonLazy getInstance() {  
    //在这个条件中判断当前是否应该加锁  
    if(instance == null) {  
        synchronized (locker) {  
            if (instance == null) {  
                instance = new SingletonLazy();  
            }        
        }    
    }    
    return instance;  
}
  • 首次调用,才会有线程安全问题(instance == null),一旦 instance 已经创建好了,不为 null,意味着此时不需要加锁了,没有线程安全问题了
  • 如果不在锁前面加上 if,性能会下降很多

3. 指令重排序导致的线程安全

指令重排序,也是一种编译器的优化方式,在不改变原有代码逻辑的条件下,有的时候调整逻辑执行顺序也能提高性能

  • 如果是单线程代码,编译器能准确地进行判断
  • 如果是多线程代码,编译器可能会出现误判
instance = new SingletonLazy();

这个语句,可以理解是分成了三个步骤:(粗略)

  1. SingletonLazy 对象分配内存空间(买房
  2. 在内存空间中,针对这个对象进行初始化(执行构造方法)(装修
  3. 将内存空间的地址,赋值给引用变量(收房拿到钥匙
    编译器可能按照 1、2、3 的顺序执行,也可能按照 1、3、2 的顺序来执行,分配内存的 1 肯定是在最前面,其他的步骤交换
    对于单线程来说,先执行 2 还是 3 本质上是一样的
    在多线程环境下,按照 132 的顺序来执行,是可能出现问题的
调度顺序线程t1t2
if(instance == null) {
synchronized (locker) {
if (instance == null) {
1. 分配内存
3. 把地址赋值给引用
(这个赋值使 instance 不再为 null
return instance;
因为在 t1 线程中,引用已经被赋值了,不再为空,所以直接返回。
instance 指向了一个没有被初始化,上面的值全为 0 的内存
2. 执行构造方法,初始化内存
  • t2 中,由于 instance 对象内存未初始化,一旦调用的方法里使用了任何 instance 的成员,都可能是错误的值,可能会引起一系列不可预期的情况

要解决因为指令重排序导致的线程安全,只需要请出 volatile 关键字就可以了

private static volatile SingletonLazy instance = null;
  • 编译器就发现了,instance 是易失的(易改变的),之后围绕这个变量的优化,就会非常的克制
  • 不仅仅会在读取变量的优化上克制,也会在修改变量的优化上克制
  • 加上 volatile 之后,也能禁止对 instance 赋值的操作插入到其他操作之间,上述 123 的操作不会再变成 132

volatile 有两个功能:

  1. 保证内存的可见性
  2. 禁止指令重排序(针对赋值)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1992052.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【性能优化】DNS解析优化

前言 DNS解析过程消耗时间DNS有本地缓存 比如首次访问某站点,会耗费很多时间进行DNS解析,但解析结束后会将ip地址存入本地设备,后续再访问此域名时就会直接从缓存中取。 首次访问页面时,本页面的DNS解析是无法优化的&#xff0…

antv l7简化版demo(含mapbox样例)

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><link rel"stylesheet" href"https://gw.alipayobjects.com/os/rmsportal/PqLCOJpqoOUfuPRacUzE.css" /><title>滑过默认高亮</…

Linux磁盘管理与文件结构(二):实用工具和命令、fdisk分区示例

文章目录 4、查看或管理磁盘分区-fdisk格式选项示例 4、示例&#xff1a;使用 fdisk 命令创建分区需求操作步骤 5、创建文件系统-mkfs格式常用选项示例创建其他类型的文件系统 6、创建文件系统-mkswap格式常用选项示例拓展&#xff1a;关闭和启用交换分区拓展&#xff1a;swap分…

路径规划 | 五种经典算法优化机器人路径规划(Matlab)

目录 效果一览基本介绍程序设计参考文献 效果一览 基本介绍 五种经典算法优化机器人路径规划&#xff0c;算法可任意更换&#xff01;地图可修改&#xff01;Matlab语言 1.分为简单路径规划和复杂路径规划两种情景&#xff0c;采用粒子群算法(PSO)&#xff0c;遗传算法(GA)&am…

[Qt][信号与槽][上]详细讲解

目录 0.Q_OBJECT宏1.信号和槽概述1.信号2.槽3.说明 2.信号和槽的使用1.连接信号和槽2.查看内置信号和槽 0.Q_OBJECT宏 Qt如果要让某个类能够使用信号槽&#xff0c;则必须要在类最开始的地方&#xff0c;写下Q_OBJECT宏 1.信号和槽概述 1.信号 在Qt中&#xff0c;⽤⼾和控件…

如何在银河麒麟操作系统上搭建 Electron (含 Electron 打包指南)

本次教程所用版本 QT版本&#xff1a;5.12 Eletron版本&#xff1a;31.3.1 Electron-packager版本&#xff1a;17.1.2 VScode版本&#xff1a;1.92.0 Node版本&#xff1a;18.19.0 npm版本&#xff1a;10.2.3 前言&#xff1a; 随着跨平台应用开发的需求日益增长&#…

Python基础核心知识点(建议收藏再用)

目录 一、python入门day1-day24 day01-03 编程语言day04 变量day05 垃圾回收机制&#xff08;GC机制&#xff09; 1 引用计数2 标记清除3 分代回收 day05 程序交互与基本运算符day06 可变不可变类型day07 流程控制 1 赋值 2 浅拷贝 copy3 深拷贝 deepcopy day08-10 基本数据类…

ES数据类型学习之keyword和text以及查询条件match和term

es&#xff08;4&#xff09;—查询条件match和term_es match term-CSDN博客 参考文章如上。开始学习 1.text和keyword的学习 直接上官网Text type family | Elasticsearch Guide [7.17] | Elastic Text type family The text family includes the following field types: …

默克索引轻松搞定,快速查找!

在化学、药学和生物科学领域&#xff0c;有一部被广泛认可的权威工具书——《默克索引》&#xff08;Merck Index&#xff09;。自1889年首次出版以来有130多年的历史&#xff0c;《默克索引》一直被视为化学品、药物和生物制品的关键物理、药理和历史信息的最权威、最可靠的来…

Ubuntu22.04安装NVIDIA Driver和CUDA

Ubuntu22.04安装NVIDIA Driver和CUDA 1.安装NVIDIA Driver(1).卸载Ubuntu自带的驱动程序&#xff1a;(2).禁用nouveau:(3).安装相应的NVIDIA Driver: 2.安装CUDA(1).下载并安装CUDA:(2).配置环境变量&#xff1a; 本文记录了在 "Ubuntu22.04"上安装 "NVIDIA D…

百度关掉Ai智能回答(保姆级技术文,解决过程完整记录)

随着AI时代到来&#xff0c;百度也是不肯落于人后&#xff0c;在其搜索页面推出了AI自动回答。点到这里的你想必正因此懊恼&#xff0c;你说它怎么切入不好&#xff0c;非得搞个东西在那一直跳&#xff0c;顶着下面的内容哐哐直跳&#xff0c;你想好好浏览内容还得等它跳消停了…

C++(week15): C++提高:(四)并发服务器架构模型

文章目录 一、五种网络IO模型1.数据传输过程2.两组重要概念3.五种网络IO模型(1)阻塞式IO(2)非阻塞式IO(3)IO多路复用(4)信号驱动IO(5)异步IO 4.五种网络IO模型的对比5.举例说明 二、并发服务器模型1.循环式迭代式模式2.并发式服务器3.prefork服务器4.反应式服务器 (Reactor)5.反…

协同过滤推荐算法(包括传统协同过滤、矩阵分解、NeuralCF)

一、什么是协同过滤推荐算法 传统的协同过滤&#xff08;Collaborative Filtering, CF&#xff09;是一种推荐系统技术&#xff0c;它基于用户的历史行为数据来预测用户对未评分项目的潜在兴趣。 “协同大家的反馈、评价和意见一起对海量的信息进行过滤&#xff0c;从中筛选出…

IPD如何通过数字化项目管理平台落地实施?

随着市场竞争的日益激烈&#xff0c;企业对于产品研发的效率和质量逐渐提高&#xff0c;越来越多的企业关注到IPD(Integrated Product Development)&#xff0c;希望参考IPD体系的方法理念和实践经验&#xff0c;从而帮助企业快速响应市场变化、缩短产品开发周期、提升产品开发…

step9:设置软件初始状态获取时不发送配置指令

文章目录 文章介绍问题描述效果图 解决办法下拉框控件ComboBox切换开关组件Switch数值微调框控件SpinBox 文章介绍 问题描述 虚拟端口&#xff08;硬件&#xff09;发送信号给客户端电脑&#xff0c;会设置此时硬件的各种基础参数&#xff0c; 客户端软件被设置基础参数之后&a…

计算机视觉——凸包计算

现在有一大堆点&#xff0c;然后你要找出一个可以围住这些点且面积最小的凸多边形&#xff0c;这个凸多边形称为凸包。 显而易见&#xff0c;如果要面积最小&#xff0c;那凸包的顶点势必得是这一大堆点的几个点&#xff0c;你也可以想成是用一条橡皮筋把这些点圈起来。 先把各…

什么是云原生?(一)

1. 前言 停下手头的工作&#xff0c;让你的同事定义“云原生”一词。你很可能会得到几个不同的答案。 1.1 让我们从一个简单的定义开始&#xff1a; 云原生架构和技术是一种设计、构建和操作在云中构建并充分利用云计算模型的工作负载的方法。 1.2 云原生计算基金会给出了官方…

easyexcel使用教程--导入导出简单案例

java中操作excel的库是poi,但是poi有很多问题&#xff0c;使用复杂&#xff0c;内存占用大等&#xff0c;阿里开源了一个操作excel的库叫easyexcel,它基于poi做了很多优化&#xff0c;平时读写excel建议使用这个库 文档地址: 关于Easyexcel | Easy Excel 官网 写入excel 在…

操作无法完成错误0x000006ba?教你几种全面解析与解决方案指南

错误代码0x000006ba通常与 Windows 操作系统中的远程过程调用&#xff08;RPC&#xff09;服务有关。当你在尝试执行某些操作&#xff0c;如连接到网络共享、运行某些网络服务或使用依赖于 RPC 的应用程序时&#xff0c;可能会遇到这个错误。接下来就和大家聊聊操作无法完成错误…

“揭秘CentosChina爬虫项目:掌握Scrapy框架的必备技巧与数据库设计“

Centoschina 项目要求 爬取centoschina_cn的所有问题&#xff0c;包括文章标题和内容 数据库表设计 库表设计&#xff1a; 数据展示&#xff1a; 项目亮点 低耦合&#xff0c;高内聚。 爬虫专有settings custom_settings custom_settings_for_centoschina_cncustom_settin…