【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题

  c96f743646e841f8bb30b2d242197f2f.gif

ddb5ae16fc92401ea95b48766cb03d96.jpeg692a78aa0ec843629a817408c97a8b84.gif


目录

1. 单例模式  

 (1) 饿汉模式  

 (2) 懒汉模式   

  1. 单线程版本  

  2. 多线程版本  

 2. 解决懒汉模式产生的线程安全问题   

  (1) 产生线程安全的原因  

  (2) 解决线程安全问题   

  1. 通过加锁让读写操作紧密执行  

方法一

方法二

  2. 处理加锁引入的新问题   

  问题描述   

  解决方法  

  3. 避免内存可见性&指令重排序  

  (1) 杜绝内存可见性问题   

  (2) 避免指令重排序问题  

1. 模拟编译器指令重排序情景

2. 指令重排序概述

3. 指令重排序类型

(1)编译器重排序

(2) 处理器重排序

4. 指令重排序所引发的问题


  1. 单例模式  

  • 单例模式能保证某个类在程序中,只存在唯 一 一 份实例,而不会创建出多个实例(不允许new多次)。
  • 要想保证单例模式只有唯一 一个实例,最重要的做法,就是用 private 修饰所有的构造方法;
  • 在 new 的过程中,会调用实例的类中的构造方法;
  • 只要用 private 修饰所有构造方法,在类外就无法获取到构造方法,进而使得 new 操作在编译时报错,因此保证某个类在程序中,只能有一份实例,而不能创建多个实例

  • 这一点在很多场景上都需要,比如 JDBC 中的 DataSource 实例就只需要一个.

单例模式具体的实现方式,分成 "饿汉" 和 "懒汉" 两种。


 (1) 饿汉模式  



下面这段代码,是对唯一成员 instance 进行初始化,用 static 修饰 instance,对 instance 的初始化,会在类加载的阶段触发;类加载往往就是在程序一启动就会触发;

由于是在类加载的阶段,就早早地创建好了实例(static修饰),这也就是“饿汉模式” 名字的由来。


在初始化好 instance 后,后续统一通过调用 getInstance() 方法获取 instance


单例模式的“点睛之笔”,用 private 修饰类中所有构造方法 



 (2) 懒汉模式   


  • 如果说,饿汉模式就是类加载的时候(一个比较早的时期)进行创建实例,并且使用private 修饰所有的构造方法,使得在代码中无法创建该类的其他实例
  • 那么懒汉方式的核心思路,就是延时的去创建实例,延时是真正用到的时候,再去创建。这样的思路,在编程中是非常有用的思路,一些情况下并不需要实例对象,通过懒汉模式来写代码,就不会去实例对象,进而可以减小开销,提升效率

  1. 单线程版本  

懒汉模式下,创建线程的时机,是在第一次使用的时候,而不是在程序启动的时候;

如果程序一启动,就要去使用实例,那 懒汉模式 和 饿汉模式 没有区别,但是程序运行很久了,都没有用到,此时懒汉模式创建实例的时间更晚一些,这样能减少不必要的开销


 2. 多线程版本  


 2.解决懒汉模式产生的线程安全问题   


  (1) 产生线程安全的原因  


  对于饿汉模式: 

而对于懒汉模式,为什么会有单线程版本和多线程版本呢?我们来看单线程版本如果运用到多线程版本,会出现什么问题:

  •  instance 被 static 修饰,多个线程调用 getInstance(),返回的是同一个内存变量;
  • 通过上面单线程版本的懒汉模式,我们可以发现,在 getInstance() 中,不但涉及了读操作,并且涉及了写操作;
  • 虽然对于图中标注的写操作,是赋值操作,并且这一步的操作是原子的,但是在多线程下调用的 getInstance() 方法,并不是原子的;
  • getInstance() 方法中,不但有写的操作,还有读的操作(满足条件才赋值,不满足条件不赋值),所以判断和赋值两个操作是紧密相连的,不能保证这两步操作紧密执行,就会出现下面的线程安全问题:

  • t1,t2如果按照上面的执行步骤,会出现值覆盖;随着第二个线程的覆盖操作,第一个线程 new 出来的对象会被 GC 回收掉。
  • 看起来没什么问题,但是 new 一个对象,会产生大量额外不必要的开销(new 一个对象的过程,可能会把大内存的数据,从硬盘加载到内存中);
  • 单例模式,不仅仅是期待只创建一个实例,更重要的是期望不要进行这种重复性的,耗时的工作,一来没意义,二来空间不允许;
  • 即使对于上面的情况,创建的第一个对象很快就被释放掉了,但是也是有数据加载过程的。

总结:

  • 饿汉模式只涉及对内存变量的读操作,不涉及写操作,因此饿汉模式是线程安全的,在单线程或者多线程的情况下,饿汉模式的基本形式不变;
  • 对于懒汉模式,在 getInstance() 中,涉及紧密相连的读写操作,但是因为读写操作不能紧密执行,导致出现线程安全问题。

  (2) 解决线程安全问题   


面试题:

这两个单例模式的 getInstance() 在多线程环境下调用,是否会出现 bug,如何解决 bug?


  1. 通过加锁让读写操作紧密执行  


对于上述饿汉模式出现线程安全问题的原因,就是读写操作(判断 + 赋值)不能紧密执行,因此,我们要对读写两步操作进行加锁,才能保证线程安全问题:

方法一

 这样加锁后,如果 t1 和 t2 还出现下图读写逻辑的执行顺序:

  • t2 会阻塞等待 t1 (或者 t1 会阻塞等待 t2)new好对象之后(读写操作结束后),释放锁,第二个线程才可以进行读写操作;
  • 此时第二个线程的判断,发现 instance != null,就会直接 return,而不会再进行实例 。

方法二

直接对 getInstance() 方法加锁,也能达到读写操作紧密执行的效果;

此时锁对象,locker  —> SingletonLazy.class,这两种方法达到的效果相同。


  2. 处理加锁引入的新问题   


  问题描述   

 

对于当前懒汉模式的代码,两个线程一把锁,是不会构成请求保持(形成死锁)的;

多个线程调用 getInstance() 方法,其实只需要保证第一个线程调用 getInstance(),执行的读写操作是紧密执行的即可,后续的线程在进行读操作发现 instance != null,就都不会触发写操作,自然就保证了线程安全;

但是按照上图的  getInstance() 方法,发现多个线程每次调用 getInstance() 都会进行一次加锁解锁操作,因为synchronized 是重量锁,多次的加锁解锁,会造成大量额外的开销,大大减低性能:

拓展:


StringBuffer 就是为了解决,大量拼接字符串时,产生很多中间对象问题而提供的一个类,提供 append insert 方法,可以将字符串添加到,已有序列的 末尾 或 指定位置。


StringBuffer 的本质是一个线程安全的可修改的字符序列,把所有修改数据的方法都加上了synchronized。但是保证了线程安全是需要性能的代价的。


在很多情况下我们的字符串拼接操作,不需要线程安全,这时候 StringBuilder 登场了,
StringBuilder 是 JDK1.5 发布的, StringBuilder  StringBuffer 本质上没什么区别,就是去掉了保证线程安全的那部分,减少了开销。所以在单线程情况下,优先考虑使用 StringBuilder


StringBuffer 和 StringBuilder 二者都继承了 AbstractStringBuilder底层都是利用可修改的 char数组 (JDK9以后是 byte 数组)。 


所以如果我们有大量的字符串拼接,如果能预知大小的话最好在new StringBuffer 或者  new StringBuilder 的时候设置好 capacity ,避免多次扩容的开销(扩容要抛弃原有数组,还要进行数组拷贝创建新的数组)。


  解决方法  

 再嵌套一次判断操作,既可以保证线程安全,又可以避免大量加锁解锁产生的开销:

  • 在单线程中,连续嵌套两层相同的 if 语句是没有意义的;因为单线程的 “执行流” 只有一个 ,嵌套两层相同的 if 语句结果相同;
  • 但是在多线程中,有多个并发执行的执行流,可能因为其中一个线程修改了 instance,导致其他线程再次执行到判断操作时,会有所不同;如上述懒汉模式,在多线程下,两个 if 得到的结果是不同的;
  • 虽然两个if相同,但是目的和作用截然不同;上面的 if,是用来判断是否需要加锁,下面的 if 判断是否需要new对象。

  • 虽然两个 if 相同,但是这只是一个巧和


  3. 避免内存可见性&指令重排序  


要杜绝可能会出现的内存可见性问题 ,并且避免指令重排序问题,只需要使用 volatile 修饰instance 即可:


  (1) 杜绝内存可见性问题   

假如不加volatile ,那么假设两个线程都执行到 synchronized 锁时, 一个线程加锁,另一个线程阻塞等待; 然后获取到锁对象的线程, 创建出来了这个单例;


释放锁之后,,另一个没获取到锁对象的线程, 获取锁之后,执行 if 判断,结果它读取的到的instance的值,是之前寄存器缓存中的值,而寄存器中缓存的 instance 还是null,因此第二个线程又回去执行锁中的逻辑,就又会去实例化一个新的 instance。


内存可见性就是保证, 每次去读取的时候,  读取到的值都是最新的值(内存中的值),而不是之前缓存在寄存器中的值;


如果不加volatile ,在上面说的案例中, 会有可能存在第二个线程获取到锁对象,结果发现这个单例(instance)是等于 null的情况;所以需要加上volatile 来保证不会出现这样的情况。


  (2) 避免指令重排序问题  

  1. 模拟编译器指令重排序情景   

要在超市中买到左边购物清单的物品,有两种买法 

 方法一:根据购物清单的顺序买;(按照程序员编写的代码顺序进行编译)

 方法二:根据物品最近距离购买;(通过指令重排序后再编译)

两种方法都能买到购物清单的所有物品,但是比起第一种方法,第二种方法在不改变原有逻辑的情况下,优化执行指令顺序,更高效地执行完所有的指令。



指令重排序在刚刚写的代码中(处理好加锁所引入的问题之后)会造成的问题

在第一个 if 结束后,可能不会直接 return,而是还有后续的逻辑;


这时候通过指令重排序,会在最开始的懒汉模式的版本,只有一个套着 synchronized 的 if 的时候,会因为加锁阻塞,而避免使用未触发对象 (instance未初始化) 的情况;


但是我们为了考虑效率,为了减少不必要的加锁操作,减少开销,我们多加了一层 if,正是多加了一层if,使得 t2 线程因为未触发 synchronized ,而不会进入阻塞等待;


所以在 t1 线程还没来得及初始化 instance 时,t2 就直接拿着未初始化的 instance 执行第一个 if 后面,后续的逻辑了


这样的问题,就是我们说的指令重排序问题。



  2. 指令重排序概述  

指令重排序是指编译器或处理器为了提高性能,在不改变程序执行结果的前提下,可以对指令序列

进行重新排序的优化技术。这种优化技术可以使得计算机在执行指令时更高效地利用计算资源,提

高程序的执行效率。


  3. 指令重排序类型  

   (1)编译器重排序   

编译器在生成目标代码时会对源代码中的指令进行优化和重排,以提高程序的执行效率。编译器重

排序时在编译阶段完成的,目的是生成更高效率的机器代码。


   (2) 处理器重排序   

处理器在执行指令也可以对指令进行重排序,以最大程度地利用处理器的流水线和多核等特性。目

的提高指令的执行效率。


   4. 指令重排序所引发的问题   

虽然指令重排序可以提高程序的执行效率,但是在多线程编程中可能会引发内存可见性问题。由于

指令重排序 可能导致共享变量的读写顺序,与代码中的顺序不一致,当多个线程同时访问共享变

量时,可能会出现数据不一致的情况。


  c96f743646e841f8bb30b2d242197f2f.gif

692a78aa0ec843629a817408c97a8b84.gif

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

论文阅读:Computational Long Exposure Mobile Photography (二)

这篇文章是谷歌发表在 2023 ACM transaction on Graphic 上的一篇文章,介绍如何在手机摄影中实现长曝光的一些拍摄效果。 Abstract 长曝光摄影能拍出令人惊叹的影像,用运动模糊来呈现场景中的移动元素。它通常有两种模式,分别产生前景模糊或…

大语言模型鼻祖Transformer的模型架构和底层原理

Transformer 模型的出现标志着自然语言处理(NLP)技术的一次重大进步。这个概念最初是针对机器翻译等任务而提出的,Transformer 后来被拓展成各种形式——每种形式都针对特定的应用,包括原始的编码器-解码器(encoder-de…

解决vue3导出.xlsx的blob文件受损问题

1、 首先要设置get或者post请求的类型。这里我用到post请求 eg&#xff1a;在http.ts中添加公共的方法。 export function post1(url: string, params: any): Promise<AxiosResponse> | Promise<AxiosResponse<any>> {return new Promise((resolve, reject…

Jest项目实战(6):搭建文档网站

搭建文档网站 创建 API 文档可以选择如下的 3 种方式&#xff1a; 功能较少&#xff0c;可以直接写在 README.md 文件里面内容较多&#xff0c;可以单独写一个文件API 数量众多&#xff08;Vue、React 这种级别&#xff09;&#xff0c;需要考虑单独拿一个网站来做详细的文档…

STM32项目---水质水位检测

1 项目简介 1.1 项目需求 本项目通过测量水体的TDS来反映水体的质量。并同时可以测量水位&#xff08;水深&#xff09;。 1.2 系统总体设计 2 硬件模块 2.1 硬件选型 水位测量模块 TDS采集模块 外置ADC模块&#xff08;ADS1115&#xff09; 2.2 水位测量模块使用方法 …

从神经元到神经网络:深度学习的进化之旅

神经元、神经网络 神经元 Neuron )&#xff0c;又名感知机( Perceptron )&#xff0c;在模型结构上与 逻辑回归 一致&#xff0c;这里以一个二维输入量的例子对其进行进一步 的解释&#xff1a; 假设模型的输 入向 量是一 维特征向 (x1,x2). 则单神 经元的模型结构 如下…

银行信贷风控专题:Python、R 语言机器学习数据挖掘应用实例合集:xgboost、决策树、随机森林、贝叶斯等

银行信贷风控专题&#xff1a;Python、R 语言机器学习数据挖掘应用实例合集&#xff1a;xgboost、决策树、随机森林、贝叶斯等 原创 拓端研究室 全文链接&#xff1a;https://tecdat.cn/?p38026 在当今金融领域&#xff0c;风险管控至关重要。无论是汽车贷款违约预测、银行挖掘…

某华迪加现场大屏互动系统mobile.do.php任意文件上传

免责声明 本文章仅供学习与交流&#xff0c;请勿用于非法用途&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任 漏洞描述 该系统是实现现场大屏互动&#xff0c;里面功能众多&#xff0c;但在mobile.do.php接口处存在任意文件上传漏洞 搜索语法 fof…

基于Python的学生宿舍管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

【AI视频换脸整合包及教程】AI换脸新星:Rope——让换脸变得如此简单

在数字技术迅猛发展的今天&#xff0c;人工智能&#xff08;AI&#xff09;的应用已经渗透到了我们生活的方方面面&#xff0c;从日常的语音助手到复杂的图像处理&#xff0c;无不体现着AI技术的魅力。特别是在娱乐和创意领域&#xff0c;AI技术更是展现出了惊人的潜力。其中&a…

A012-基于Spring Boot的私房菜定制上门服务系统的设计与实现

摘 要 如今社会上各行各业&#xff0c;都喜欢用自己行业的专属软件工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。新技术的产生&#xff0c;往往能解决一些老技术的弊端问题。因为传统私房菜定制上门服务系统信息管理难度大&#xff0c;容错率…

EV录屏好用吗?盘点2024年10款专业好用的录屏软件。

EV录屏的方式有很多种&#xff0c;它设置了很多模式&#xff0c;并且录制高清&#xff0c;可以免费使用。但是现在很多的录屏工具都有着与这个软件相似的功能&#xff0c;在这里我可以给大家列举一些。 1、福昕电脑录屏 这个软件为用户提供了多种录制模式&#xff0c;让视频录…

【网易云插件】听首歌放松放松

先看效果&#xff1a; 网易云有两种类似的插件。 第一种 &#xff1a; iframe 优点&#xff1a;可以自己调整插件的高度、宽度 缺点&#xff1a;很多博客网站不支持嵌入iframe&#xff0c;请试一下您的网站是否支持 登录可直接复制代码。 也可以在我这里 <iframe fram…

Java的Object类常用的方法(详述版本)

文章目录 一、什么是Object类二、常用方法&#xff1a;toString&#xff08;&#xff09;三、常用方法&#xff1a;对象比较equals&#xff08;&#xff09;四、常用方法&#xff1a;hashcode&#xff08;&#xff09;五、总结 一、什么是Object类 顾名思义&#xff0c;Object类…

SQL--查询连续三天登录数据详解

问题&#xff1a; 现有用户登录记录表&#xff0c;请查询出用户连续三天登录的所有数据记录 id dt1 2024-04-25 1 2024-04-26 1 2024-04-27 1 2024-04-28 1 2024-04-30 1 2024-05-01 1 2024-05-02 1 2024-05-04 1 2024-05-05 2 20…

docker+mysql配置

拉取mysql docker pull mysqlmysql配置 创建存储文件夹 mkdir -p /home/mysql/{date,conf}在conf文件中配置my.cnf sudo vim my.cnfmy.cnf具体配置 [mysqld] #Mysql服务的唯一编号 每个mysql服务Id需唯一 server-id1#服务端口号 默认3306 port3306#mysql安装根目录&#x…

qt QHeaderView详解

1、概述 QHeaderView 是 Qt 框架中的一个类&#xff0c;它通常作为 QTableView、QTreeView 等视图类的一部分&#xff0c;用于显示和管理列的标题&#xff08;对于水平头&#xff09;或行的标题&#xff08;对于垂直头&#xff09;。QHeaderView 提供了对这些标题的排序、筛选…

删除 需要来自XXXX的权限才能对此文件夹进行更改 文件的解决办法

如果你也是&#xff1a; 如果你也有类似上面的问题&#xff0c;这篇文章算是你看对了&#xff0c;哦哟&#xff01; 我的牙齿现在是怨灵的牙齿&#xff0c;可以啃下一头牛。 翻遍千山万水&#xff0c;咱们也是终于取到真经了家人们。 首先下一个everything好吗 甩一个官网链…

题目练习之二叉树那些事儿(续集)

♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥ ♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥ ♥♥♥我们一起努力成为更好的自己~♥♥♥ ♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥ ♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥ 这一篇博客我们继…

本地Docker部署ZFile网盘打造个人云存储,告别公共网盘让你数据安全感爆棚

文章目录 前言1.关于ZFile2.本地部署ZFile3.ZFile本地访问测试4.ZFile的配置5.cpolar内网穿透工具安装6.创建远程连接公网地址7.固定ZFile公网地址 前言 本文主要介绍如何在Linux Ubuntu系统使用Docker本地部署ZFile文件管理系统&#xff0c;并结合cpolar内网穿透工具实现远程…