【JavaEE初阶】深入解析死锁的产生和避免以及内存不可见问题

前言:

🌈上期博客:【后端开发】JavaEE初阶—线程安全问题与加锁原理(超详解)-CSDN博客

🔥感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客

⭐️小编会在后端开发的学习中不断更新~~~

🥳非常感谢你的支持

 

目录

📚️1.引言

📚️2.可重入锁

2.1概念

 2.2原理理解

 📚️3.死锁

 3.1产生死锁的情况

1.一个线程,一把锁

2.两个线程,两把锁

 3.N个线程,M把锁

3.2解决死锁的方法

📚️4.内存可见性

4.1内存可见性实例

4.2内存可见性原理

 4.3内存可见性解决

1.进行线程休眠

2.添加volatile关键词 

📚️5.总结


📚️1.引言

      OK啊!!!小伙伴们,本小编又带来了一个重磅知识,我们上期讲解了关于线程安全问题,引出了加锁这个概念;但是加锁会产生一个严重的问题,就是当我们运用不当时,进行加锁会导致死锁的发生,那怎样才会导致死锁呢?以及如何避免呢?这就是小编本期的重要内容;

发车发车gogogog~~~🥳🥳🥳;

且听小编讲解,包你学会!!! 

📚️2.可重入锁

2.1概念

什么是可重入锁呢???,让我们看看以下代码:

 public static void main(String[] args) {Object lock=new Object();//可重入锁实例Thread t1=new Thread(()->{synchronized (lock){synchronized (lock){System.out.println("Hello thread");}}});t1.start();        }

对于如何进行加锁操作,小编上期有讲,不清楚的小伙伴可以自己去看看哦~~~

开始认知:这里由于lock已经被加过一次锁了,那么接下来再加一次锁,不会发生线程阻塞吗,第一次加又没有进行释放; 

注意:上面这种理解完全是错误的,这里就是由于使用同一个线程,此时的锁对象,就能够知道第二次加锁的线程,是持有锁的线程,那么在第二次加锁时,就直接通过,就不会发生“阻塞”现象

这种特性叫:可重入性,这个锁就叫做可重入锁~~~

 2.2原理理解

在Java中实现可重入锁是非常简单的,因为synchrinized自带这个特性,那么这个特性的内部原理是啥呢,且看如下图所示:

注意:对于重入锁来说,最主要两部分第一个就是对于加锁的线程是否为同一个线程,第二就是对于加锁过程的计数器的次数理解;

以上都是在java中synchronized封装实现的,那么在C++中就没有重入锁的概念,此时当存在复杂的调用关系的时候,就会存在卡死的情况,就是“死锁”,接下来就注重“死锁”的理解;

 📚️3.死锁

在之前讲解过,加锁可以解决线程安全问题,但是操作不当会产生“死锁”的情况;

 3.1产生死锁的情况

1.一个线程,一把锁

即在上述讲解过程中的可重入所情况,但是如果没有可重入这个性质,那么连续对一个线程加锁两次,那么就会产生死锁;

2.两个线程,两把锁

即有两个线程,当线程1加上锁A,线程2加上锁B,那么然后在两个锁不进行释放的前提下,双方都想拿到对方的锁,此时就会发生死锁的情况;

这里有代码进行演示:

Object A=new Object();//对象AObject B=new Object();//对象B//创建线程t1拿到锁Thread t1=new Thread(()->{synchronized (A) {System.out.println("线程t1拿到了锁A");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException();}//线程进入休眠,保证另一个线程也能够拿到锁//尝试拿到对方的锁,此时锁A没有释放synchronized (B) {System.out.println("线程t1拿到了两把锁");}}});

此时小编设置了两个对象,即两个锁对象,那么我们就进行线1的加锁,此时当我们拿到锁A后,在不解开锁的情况下进行另外一把锁B的获取:

Thread t2=new Thread(()->{synchronized (B) {System.out.println("线程t2拿到了锁B");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException();}//线程进入休眠,保证另一个线程也能够拿到锁//尝试拿到对方的锁,没有释放自己的锁synchronized (A) {System.out.println("线程t2拿到了两把锁");}}});

此时,我们就行第二个线程的实现,和上述线程1一样,当拿到自己的锁之后,在不解开锁的情况下进行锁A的获取,然后两个线程启动之后的结果就是:

此时可以发现,线程各自拿到自己的锁之后,就直接“卡住”了,这就发生了线程安全问题;

当我们打开jconsole后,可以看看我们的线程情况:

这是我们的两个对应的线程名字,此时两个线程的执行状态就如下图所示:

注意:此时可以看到线程1处于BLOCKED状态,并且在等锁B,那么锁B的拥有者是线程2;同理,线程2在等锁A,而锁A的拥有者就是线程1;

可以发现此时两个锁都在等对方释放锁,此时就产生了死锁;

 3.N个线程,M把锁

此时这种情况就是要考虑到“哲学家就餐问题了”,什么是哲学家就餐问题呢???

解释:此时有5个哲学家要吃面,但是筷子只有5根,这无根筷子在每个哲学家之间,此时就是五个哲学家就是五个线程筷子就是锁,当每个哲学家拿到筷子后,旁边的两哲学家是吃不到的,就处于阻塞的状态,一般情况下哲学家啥时候吃到面是一个随机问题,一般情况下这是没有问题的~~~

注意:当我们每个哲学家左手拿起筷子时,可以发现此时每个哲学家都吃不到面(吃面要两根筷子),都等待另一个哲学家释放筷子(锁),此时就发生了线程的阻塞 ;

3.2解决死锁的方法

在了解线程的解决死锁之前我们要知道产生死锁的必要条件

(重点)

1.互斥使用:当一个线程获取到锁之后,另一个线程也想要获取,那么此时就要进行阻塞

2.不可抢占:当一个线程获得锁之后,其他线程想要获取此时就要等到锁的释放,不能强行占用

3.请求保持:当一个线程获得锁A之后,尝试再次获取锁B(锁A是没有释放的)

4.循环等待/环路等待

以上就是死锁形成的必要条件,缺一不可~~~;

那么针对以上死锁的产生条件,第三个条件是根据具体的代码来进行实现的,但是我们可以根据最后一个来进行攻破;

注意:解决哲学家问题关键:针对五把锁我们可以对其进行编号,然后每个哲学家也进行编号,此时约定,每个哲学家开始只能够拿编号比自己小的锁,然后再拿比自己编号大的锁 

为啥能够解决死锁问题呢???且看下图所示:

过程解释:当我们为这个线程和锁进行编号后,此时由于只能拿比自己编号小的筷子,那么2号哲学家拿1筷子,3号哲学家拿2号筷子.......到最后,5号哲学家拿4号筷子,此时就可以发现多了一双筷子,那么5号哲学家先再拿起5号筷子,此时当5号哲学家吃完后,放下筷子,一次类推,可以保证每个哲学家都拿够吃到面~~~,同理这就解决了死锁这个问题;

那么此时我们就可以改变之前这个双方获取两个锁的这个代码,实现这个代码的可行性:

Thread t2=new Thread(()->{synchronized (A) {System.out.println("线程t2拿到了锁A");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException();}//线程进入休眠,保证另一个线程也能够拿到锁//尝试拿到对方的锁,没有释放自己的锁synchronized (B) {System.out.println("线程t2拿到了两把锁");}}});

注意:这里小编只改了第二个线程的获取锁的顺序,即可保证两个线程都能够拿到锁;

解析:这里能够执行原因:当两个线程启动的时候,线程1获取到了锁A,所以此时线程B规定先不获取锁,即他以获取锁A来发生阻塞,当线程1执行完后,线程2就能够得到两个线程了~~~,这就是引入了加锁顺序规则~~~

 总结:

解决死锁有很多办法,以下是一些小编总结的一些方法:

1.添加“筷子”;

2.去掉一个线程

3.引入计数器,规定最多同时几个人吃面

4.引入加锁顺序规则

5.“银行家算法”

1~3:虽然能够解决这个问题,但是普适性不高;

4:是小编推荐的,普适性高,而且容易实现;

5:是可以解决这个死锁问题,但是不推荐,实现过程很复杂,理论成立,现实不行;

📚️4.内存可见性

4.1内存可见性实例

内存可见性问题:即一个程序读,一个程序写的过程中产生的线程安全问题;

小编就用代码实例来演示:

public static void main(String[] args) {Thread t1=new Thread(()->{while (flag==0){//不输出任何;               }System.out.println("flag的值进行了改变");});Thread t2=new Thread(()->{System.out.println("输入一个flag的值");Scanner scanner=new Scanner(System.in);flag=scanner.nextInt();});//起启动线程t1.start();t2.start();}

解释:此时我们规定线程1进行读的操作,若flag是0,那么就不会打印任何日志,此时小编在线程2上进行改变,线程1中flag的值,让其不满足条件,实现跳出循环,结束线程;但是输出如下:

可以发现在小编输入1,改变flag的值之后,回车并没有输出“flag的值进行了改变”,所以此时就发生了线程安全问题,即内存可见性问题~~~

4.2内存可见性原理

这里从核心指令入手:

while (flag==0){//不输出任何;               
}

注意:这里的核心指令有两条

1.load读取内存当中的指令到寄存器中

2.寄存器拿着值与0进行比较

那么此时就是在线程2启动到输入这个操作,不断进行循环读取,比较的过程,所以这个操作有两个关键要点:

1.load不断从内存中读取数据到CPU寄存器上,这个操作的执行结果是一样的,几秒之内已经很多次了~~~

2.load从内存中读取数据这个操作开销远远大于寄存器比较这个操作~~~

此时就出现了一个问题:编译器优化代码这个操作,即JVM在优化中发现读取数据操作一直不变,那么优化后即将这个load读取数据操作给省去了(关键原因); 

代码优化:即JVM在保持原有代码逻辑不变的情况下,实现提高代码的效率,单线程还好,但是多线程很容易发生误判~~~

 4.3内存可见性解决

1.进行线程休眠

核心:在上述讲解中,是因为读取这个内存的次数过多,且没有改变,所以我们能够实现,读取次数减少的操作;

代码实现如下:

Thread t1=new Thread(()->{while (flag==0){//不输出任何;try {Thread.sleep(1000);}catch (InterruptedException e){e.printStackTrace();}}System.out.println("flag的值进行了改变");});

小编只需要在读取数据这个操作实现休眠即可;

注意:这里的休眠是为了减少从内存中读取数据到CPU寄存器上,让load开销减少,减少迫切优化的程度;此时JVM就不会进行优化了,那么就不会出现线程安全问题

2.添加volatile关键词 

volatile作用:这里的volatile关键词会阻止JVM对程序进行优化,确保每次循环都会从内存中读取数据到寄存器当中~~~

volatile核心作用:解决内存可见性问题,和禁止指令重排序~~~

代码演示:

public  volatile static int flag=0; //加上volatile实现代码优化的消除public static void main(String[] args) {Thread t1=new Thread(()->{while (flag==0){//不输出任何;                }System.out.println("flag的值进行了改变");});

注意:volatile和上述休眠作用基本一致,都是使JVM优化程序关闭,保证每次循环都是从内存中读取数据,而不是优化成直接从寄存器当中读取数据~~~

📚️5.总结

💬💬本期小编总结了关于多线程的重要知识即死锁,分别从造成原因和如何进解决提出了关于小编的理解,以及线程安全问题之内存可见性问题,并附上了代码供小伙伴们参考参考~~~

 🌅🌅🌅~~~~最后希望与诸君共勉,共同进步!!!


💪💪💪以上就是本期内容了, 感兴趣的话,就关注小编吧。

                                                               😊😊  期待你的关注~~~

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

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

相关文章

C#图像处理学习笔记(屏幕截取,打开保存图像、旋转图像、黑白、马赛克、降低亮度、浮雕)

1、创建Form窗体应用程序 打开VS,创建新项目-语言选择C#-Window窗体应用(.NET Framework) 如果找不到,检查一下有没有安装.NET 桌面开发模块,如果没有,需要下载,记得勾选相关开发工具 接上一步,…

【UE5】将2D切片图渲染为体积纹理,最终实现使用RT实时绘制体积纹理【第四篇-着色器投影-接收阴影部分】

上一章中实现了体积渲染的光照与自阴影,那我们这篇来实现投影 回顾 勘误 在开始本篇内容之前,我已经对上一章中的内容的错误进行了修改。为了确保不会错过这些更正,同时也避免大家重新阅读一遍,我将在这里为大家演示一下修改的…

叉车司机信息权限采集系统,保障与优化叉车运输网络的安全

叉车司机信息权限采集系统可以通过监控司机的行车行为和车辆状况,实时掌握车辆位置和行驶路线,从而提高运输安全性,优化运输网络,降低事故风险。同时,该系统还可以通过对叉车司机信息和行车数据的分析,优化…

Flutter屏幕适配

我们可以根据下面有适配属性的Widget来进行屏幕适配 1.MediaQuery 通过它可以直接获得屏幕的大小(宽度 / 高度)和方向(纵向 / 横向) Size screenSize MediaQuery.of(context).size; double width screenSize.width; double h…

springboot异常(三):异常处理原理

🍅一、BasicErrorController ☘️1.1 描述 BasicErrorController是Springboot中默认的异常处理方法,无需额外的操作,当程序发生了异常之后,Springboot自动捕获异常,重新请求到BasicErrorController中,在B…

网络安全 DVWA通关指南 DVWA Stored Cross Site Scripting (存储型 XSS)

DVWA Stored Cross Site Scripting (存储型 XSS) 文章目录 DVWA Stored Cross Site Scripting (存储型 XSS)XSS跨站原理存储型 LowMediumHighImpossible 参考文献 WEB 安全靶场通关指南 相关阅读 Brute Force (爆破) Command Injection(命令注入) Cro…

Spring:项目中的统一异常处理和自定义异常

介绍异常的处理方式。在项目中,都会进行自定义异常,并且都是需要配合统一结果返回进行使用。 1.背景引入 (1)背景介绍 为什么要处理异常?如果不处理项目中的异常信息,前端访问我们后端就是显示访问失败的…

eslint-plugin-react的使用中,所出现的react版本警告

记一次使用eslint-plugin-react的警告 Warning: React version not specified in eslint-plugin-react settings. See https://github.com/jsx-eslint/eslint-plugin-react#configuration . 背景 我们在工程化项目中,常常会通过eslint来约束我们代码的一些统一格…

基于RPA+BERT的文档辅助“悦读”系统 | OPENAIGC开发者大赛高校组AI创作力奖

在第二届拯救者杯OPENAIGC开发者大赛中,涌现出一批技术突出、创意卓越的作品。为了让这些优秀项目被更多人看到,我们特意开设了优秀作品报道专栏,旨在展示其独特之处和开发者的精彩故事。 无论您是技术专家还是爱好者,希望能带给…

关于寻址方式的讨论

### 对话内容 **学生B(ESFP)**:老师,寻址方式听起来很复杂,能详细讲解一下吗?而且最好能举些具体例子!😊 **老师(ENTP)**:当然可以!…

JVM(HotSpot):方法区(Method Area)

文章目录 一、内存结构图二、方法区定义三、内存溢出问题四、常量池与运行时常量池 一、内存结构图 1.6 方法区详细结构图 1.8方法区详细结构图 1.8后,方法区是JVM内存的一个逻辑结构,真实内存用的本地物理内存。 且字符串常量池从常量池中移入堆中。 …

蓝队技能-应急响应篇Web内存马查杀Spring框架型中间件型JVM分析Class提取

知识点: 1、应急响应-Web框架内存马-分析&清除 2、应急响应-Web中间件内存马-分析&清除 注:框架型内存马与中间件内存马只要网站重启后就清除了。 目前Java内存马具体分类: 1、传统Web应用型内存马 Servlet型内存马:…

vivado中除法器ip核的使用

看了很多博客,都没写清楚,害 我要实现 reg [9:0] a; 被除数 reg [16:0] b; 除数 wire [39:0] res; 结果 wire [15:0] real_shan; 要实现a/b 则如下这么配置 选择经过几个周期出结果 wire [39:0] res; // dly5 div_gen_0 div_gen_0_inst (.aclk(clk), …

精密制造的革新:光谱共焦传感器与工业视觉相机的融合

在现代精密制造领域,对微小尺寸、高精度产品的检测需求日益迫切。光谱共焦传感器凭借其非接触、高精度测量特性脱颖而出,而工业视觉相机则以其高分辨率、实时成像能力著称。两者的融合,不仅解决了传统检测方式在微米级别测量上的局限&#xf…

通过 LabVIEW 正则表达式读取数值(整数或小数)

在LabVIEW开发中,字符串处理是一个非常常见的需求,尤其是在处理包含复杂格式的数字时。本文通过一个具体的例子来说明如何利用 Match Regular Expression Function 和 Match Pattern Function 读取并解析字符串中的数字,并重点探讨这两个函数…

MyBatis<foreach>标签的用法与实践

foreach标签简介 实践 demo1 简单的一个批量更新&#xff0c;这里传入了一个List类型的集合作为参数&#xff0c;拼接到 in 的后面 &#xff0c;来实现一个简单的批量更新 <update id"updateVislxble" parameterType"java.util.List">update model…

计算机视觉学习路线

计算机视觉&#xff08;Computer Vision&#xff09;是计算机科学的一个重要分支&#xff0c;旨在使计算机能够理解和解释视觉数据。以下是一个详细的计算机视觉学习路线&#xff0c;帮你系统地掌握这个领域所需的知识和技能。 1. 基础数学和编程 在深入学习计算机视觉之前&…

希捷电脑硬盘好恢复数据吗?探讨可能性、方法以及注意事项

在数字化时代&#xff0c;数据已成为我们生活和工作中不可或缺的一部分。希捷电脑硬盘作为数据存储的重要设备&#xff0c;承载着大量的个人文件、工作资料以及珍贵回忆。然而&#xff0c;面对硬盘故障或误操作导致的数据丢失&#xff0c;许多用户不禁要问&#xff1a;希捷电脑…

毕业设计选题:基于ssm+vue+uniapp的鲜花销售小程序

开发语言&#xff1a;Java框架&#xff1a;ssmuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;M…

FLUX.1图像生成模型:AI工程师的实践与探索

文章目录 1 FLUX.1系列模型2 AI工程师的视角3 ComfyUI部署4 FLUX.1部署5 工作流6 面向未来 黑森林实验室&#xff08;Black Forest Labs&#xff09;研发的FLUX.1图像生成模型&#xff0c;以其120亿参数的庞大规模&#xff0c;正在重新定义图像生成技术的新标准。FLUX.1系列模型…