【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 后面,后续的逻辑了

左边是 多线程版本 的 懒汉模式 中的 getInstance() 要执行的操作,右边是多个线程调用 getInstance();

如果真的因为指令重排序,而导致 内存空间的首地址,赋值给引用变量(拿到钥匙) 的操作,被重排序到 初始化 instance 之前(装修),就会出现下面的问题:


在一个线程已经获取锁对象,进行加锁,还未来得及初始化 instance 时,另一个线程会因为最外层的 if 语句,而跳过 if 中代码块里的加锁操作,而避免了阻塞等待;


如果后续的 getInstance() 在第一层 if 语句后,还有其他逻辑,第二个线程就会拿着未被初始化的 instance 来进行后面的逻辑。


这就是指令重排序问题,而使用 volatile 修饰 instance 后,不但能确保每次读取操作,都是读内存,而且关于该变量的读取和修改操作,不会触发重排序


  2. 指令重排序概述  

指令重排序是指编译器或处理器为了提高性能,在不改变程序执行结果的前提下,可以对指令序列进行重新排序的优化技术。这种优化技术可以使得计算机在执行指令时更高效地利用计算资源,提高程序的执行效率。

JMM(Java 内存模型)详解


   指令重排序   


为了提升执行速度/性能,计算机在执行程序代码的时候,会对指令进行重排序。


   什么是指令重排序?   


简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。


   常见的指令重排序有下面2种情况:   


  (1) 编译器优化重排:   


编译器(包括JVM、JIT编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。


   (2) 指令并行重排:   


现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。


另外,内存系统也会有“重排序”,但又不是真正意义上的重排序。在JMM里表现为主存和本地内存的内容可能不一致,进而导致程序在多线程下执行可能出现问题。


Java 源代码会经历编译器优化重排—>指令并行重排一>内存系统重排的过程,最终才变成操作系统可执行的指令序列。


指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致,所以在多线程下,指令重排序可能会导致一些问题。


对于编译器优化重排,和处理器的指令重排序(指令并行重排和内存系统重排都属于是处理器级别的指令重排序),处理该问题的方式不一样。


  • 对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。
  • 对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。

内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。


另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障变量的可见性。


  3. 指令重排序类型  

   (1)编译器重排序   

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


   (2) 处理器重排序   

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


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

虽然指令重排序可以提高程序的执行效率,但是在多线程编程中可能会引发内存可见性问题。由于指令重排序 可能导致共享变量的读写顺序,与代码中的顺序不一致,当多个线程同时访问共享变量时,可能会出现数据不一致的情况。


  c96f743646e841f8bb30b2d242197f2f.gif

692a78aa0ec843629a817408c97a8b84.gif

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

二叉树搜索树(下)

二叉树搜索树(下) 二叉搜索树key和key/value使用场景 key搜索场景 只有key作为关键码,结构中只需要存储key即可,关键码即为需要搜索到的值,搜索场景只需要判断 key在不在。key的搜索场景实现的二叉树搜索树支持增删查…

Web项目版本更新及时通知

背景 单页应用,项目更新时,部分用户会出更新不及时,导致异常的问题。 技术方案 给出版本号,项目每次更新时通知用户,版本已经更新需要刷新页面。 版本号更新方案版本号变更后通知用户哪些用户需要通知?…

D64【python 接口自动化学习】- python基础之数据库

day64 SQL-DQL-基础查询 学习日期:20241110 学习目标:MySQL数据库-- 133 SQL-DQL-基础查询 学习笔记: 基础数据查询 基础数据查询-过滤 总结 基础查询的语法:select 字段列表|* from 表过滤查询的语法:select 字段…

Unity插件-Smart Inspector 免费的,接近虚幻引擎的蓝图Tab管理

习惯了虚幻的一张蓝图,关联所有Tab (才发现Unity,的Component一直被人吐槽,但实际上是:本身结构Unity 的GameObject-Comp结构,是好的不能再好了,只是配上 smart Inspector就更清晰了&#xff0…

2024 年Postman 如何安装汉化中文版?

2024 年 Postman 的汉化中文版安装教程

单元测试、集成测试、系统测试、验收测试、压力测试、性能测试、安全性测试、兼容性测试、回归测试(超详细的分类介绍及教学)

目录 1.单元测试 实现单元测试的方法: 注意事项: 2.集成测试 需注意事项: 实现集成测试的方法: 如何实现高效且可靠的集成测试: 3.系统测试 实现系统测试的方法: 须知注意事项: 4.验收测试 实现验…

MySQL 忘记 root 密码,使用跳过密码验证进行登录

操作系统版本:CentOS 7 MySQL 忘记 root 密码,使用跳过密码验证进行登录 修改 /etc/my.cnf 配置文件,在 [mysqld] 后面任意一行添加 skip-grant-tables vim /etc/my.cnf 重启 MySQL systemctl restart mysqld 登录 MySQL(无 -…

3D Web渲染引擎HOOPS Communicator:助力企业打造定制化3D可视化产品的强大工具

HOOPS Communicator为开发人员提供了多样化的定制手段,使其在3D网页可视化领域保持领先地位。很多潜在客户都关心如何利用HOOPS Communicator将其打造成自己产品的独特解决方案。展示我们现有合作伙伴的成功案例正是分享此信息的最佳方式。 每家合作伙伴都在产品中…

【stablediffusion】阿里发布新ID保持项目EcomID, 可从单个ID参考图像生成定制的保ID图像,ComfyUI可使用。

今天,我们将向您介绍一款令人兴奋的更新——阿里发布的ID保持项目EcomID。这是一款基于Stable Diffusion技术的AI绘画工具,旨在为您提供一键式生成高质量保ID图像的便捷体验。无论您是AI绘画的新手还是专业人士,这个工具都能为您带来极大的便…

计算机网络(11)和流量控制补充

这一篇对数据链路层中的和流量控制进行详细学习 流量控制(Flow Control)是计算机网络中确保数据流平稳传输的技术,旨在防止数据发送方发送过多数据,导致接收方的缓冲区溢出,进而造成数据丢失或传输失败。流量控制通常…

【VLANPWN】一款针对VLAN的安全研究和渗透测试工具

关于VLANPWN VLANPWN是一款针对VLAN的安全研究和渗透测试工具,该工具可以帮助广大研究人员通过对VLAN执行渗透测试,来研究和分析目标VLAN的安全状况。该工具专为红队研究人员和安全学习爱好者设计,旨在训练网络工程师提升网络的安全性能&…

ES6代理和反射新特性,详细讲解

代理与反射 es6新增了代理和反射特性&#xff0c;这两个特性为开发者提供了拦截并向基本操作嵌入额外行为的能力。 代理基础 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta charset"UTF-8"&g…

MYSQL 精通索引【快速理解】

目录 1、什么是索引&#xff1f; 2、索引结构 1.为什么不使用二叉树呢&#xff1f; 2.B树数据结果 3.B树 4.Hash结构 3、索引语法 1.创建索引 2.查看索引 3.删除索引 4、SQL性能分析 1.SQL执行频次 2.慢查询日志 3.profile详情 4.EXPLAIN 5、索引规则 1.最左前缀法则 2.索…

光驱验证 MD5 校验和

步骤 1&#xff1a;在 Ubuntu 上打包文件并生成 MD5 校验和 打包文件 使用 tar 命令将文件夹打包成 tar.gz 文件&#xff1a; tar -czvf my_files.tar.gz /path/to/folder 生成 MD5 校验和 使用 md5sum 命令生成打包文件的 MD5 校验和&#xff1a; md5sum my_files.tar.g…

《网络数据安全管理条例》将于2025年1月1日起正式施行,从业者应如何解读?

2024年9月&#xff0c;国务院总理李强签署国务院令&#xff0c;公布了《网络数据安全管理条例》&#xff08;以下简称《条例》&#xff09;&#xff0c;该条例将于2025年1月1日起正式施行。 这一条例的出台&#xff0c;标志着我国在网络数据安全领域的管理迈上了新的台阶&#…

【MMIN】缺失模态想象网络用于不确定缺失模态的情绪识别

代码地址&#xff1a;https://github.com/AIM3RUC/MMIN abstract&#xff1a; 在以往的研究中&#xff0c;多模态融合已被证明可以提高情绪识别的性能。然而&#xff0c;在实际应用中&#xff0c;我们经常会遇到模态丢失的问题&#xff0c;而哪些模态会丢失是不确定的。这使得…

【Java Web】监听器类型及其使用

文章目录 监听器使用监听器类型ServletContextListenerHttpSessionListenerServletRequestListenerServletContextAttributeListenerHttpSessionAttributeListenerServletRequestAttributeListenerHttpSessionBindingListener 监听器&#xff08;Listener&#xff09;组件用于监…

conda创建 、查看、 激活、删除 python 虚拟环境

1、创建 python 虚拟环境 ,假设该环境命名为 “name”。 conda create -n name python3.11 2、查看 python 虚拟环境。 conda info -e 3、激活使用 python 虚拟环境。 conda activate name 4、删除 python 虚拟环境 conda remove -n name --all ​​ 助力快速掌握数据集…

LaTeX之四:如何兼容中文(上手中文简历和中文论文)、在win/mac上安装新字体。

改成中文版 如果你已经修改了.cls文件和主文档&#xff0c;但编译后的PDF仍然显示英文版本&#xff0c;可能有以下几个原因&#xff1a; 编译器问题&#xff1a;确保你使用的是XeLaTeX或LuaLaTeX进行编译&#xff0c;因为它们对Unicode和中文支持更好。你可以在你的LaTeX编辑器…

视频遥控打药履带机器人技术详解

视频遥控打药履带机器人技术是一种集成了遥控操作、视频监控和履带行走系统的现代化农业植保技术。以下是对该技术的详细解析&#xff1a; 一、技术概述 视频遥控打药履带机器人主要由履带行走系统、药箱、喷雾系统、遥控系统以及视频监控系统等部分组成。通过遥控操作&#…