【JavaEE初阶 — 多线程】死锁的产生原因和解决方法

 c96f743646e841f8bb30b2d242197f2f.gif

ddb5ae16fc92401ea95b48766cb03d96.jpeg692a78aa0ec843629a817408c97a8b84.gif


目录

  死锁  

  1.构成死锁的场景  

  (1) 一个线程一把锁   

  问题描述  

  解决方案(可重入锁)  

  (2) 两个线程两把锁  

  问题描述   

  (3)N个线程 M把锁   

  哲学家就餐问题  

  2.死锁的四个必要条件 

  3.如何解决死锁问题  

  (1)避免出现请求和保持  

  (2)打破多个线程的循环等待关系 


  死锁  


  1.构成死锁的场景  


  (1) 一个线程一把锁   


  问题描述  


 我们先来看下面的代码: 

c18ffdbb5cc04c9f8a06d4d2200e2a06.png看起来是两次对同一个引用的加锁,是没有必要的,但是在我们日常学习和工作中,很容易就会写出上述的对一个锁对象进行两次或者多次加锁的操作,此时会出现如下情况:

8a91473519cc4d308594382f01cac541.png

  • 如果第一次加锁要解锁,就必须得先执行完{}中的代码块,就必须进行第二次加锁;
  • 但是第二次加锁时,发现锁对象 locker 还未被解锁,第二次加锁因此进入阻塞等待的状态,所以第一次加锁的操作无法执行到解锁的位置;
  • 上面的这种情况,就被称为“死锁”(dead lock);死锁是一个非常严重的 bug ,一旦出现,整个线程都会被卡住。

上面的代码虽然会造成死锁,但是我们不太容易写出上面的代码;

但是一旦方法调用的层次比较深,就容易出现对同一对象进行多次加锁的情况。 我们再来分析下面的代码:

bdacc8d6936c476bbd6522be3c0685b0.png

  1. 第一次进行加锁操作,能够成功的(锁对象还没有被获取);
  2. 第二次进行加锁,此时意味着,锁对象是已经被占用的状态;第二次加锁,就会触发阻塞等待。

26fb071c000849ac95f85d33308b7eeb.png


  解决方案(可重入锁)  


为了解决上述代码出现的死锁问题, Java 的 synchronized 就引入了可重入的概念;

当 t线程 对 locker对象 加锁成功之后,后续 t 再次针对 locker 进行加锁,不会触发阻塞,而是直接往下走,因为当前 locker 就是被 t 持有~~

ebea33130b7c4b4cb874e110c8e73e04.png


但是,如果是其他线程尝试加锁,就会正常进入阻塞等待的状态;


805cc82e2ba54726a92301bee54b74b4.png


  • 如果发现是同一个锁持有者的线程,则跳过加锁环节;
  • 如果是不同的锁持有者,才会进入阻塞等待。

我们运行刚刚所写的代码,发现程序是可以正常执行的:

118d085356bd478bb7344f8a8f6c9974.png

  • 理论上,程序会被上死锁,但是当我们正在运行程序时,会发现程序依旧可以正常执行,输出结果也正确;
  • 这样的原因是因为 synchronized 的可重入性,解决了当前情况(一个线程针对同一个锁对象进行多次加锁)造成的死锁的问题,哪怕我们再锁三四层,synchronized 的可重入性都会解决该问题。

可重入锁只能针对 一个线程多次对锁对象进行加锁 的情况,如果是其他情况造成的死锁,则无法通过可重入锁解决。

面试官的问题:
如何自己实现一个可重入锁?

  1. 在锁内部记录当前是哪个线程持有的锁,后续每次加锁,都进行判定
  2. 通过计数器,记录当前加锁的次数,从而确定何时真正进行解锁.

  (2) 两个线程两把锁  


  问题描述   


  • 现在有 t1,t2 两个线程,以及 locker1,locker2 两把锁;
  • t1 获取 locker1,t2 获取 locker2 后,t1,t2再分别尝试获取 locker2,locker1,两个线程互不相让,因此进入阻塞等待,最终造成死锁的情况;(家钥匙放车里,车钥匙放家里);

因为上述这种情况,构成的死锁问题的原因,不但因为锁互斥与不可抢占的性质,也因为两个线程在加锁的过程中,造成了请求保持和循环等待;

而造成死锁的原因,是因为两个线程对两个锁对象的加锁,是嵌套的写法;并且两个线程在阻塞等待的过程,等待关系形成了循环


我们来看下面这段代码:

package Thread;public class Demo22 {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {System.out.println("t1 获取到 locker1");synchronized (locker1){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1 获取到 locker2");}}});Thread t2 = new Thread(() ->{synchronized (locker2){System.out.println("t2 获取到 locker2");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){System.out.println("t2 获取到 locker1");}}});t1.start();t2.start();t1.join();t2.join();}
}

代码逻辑:

  1. t1 线程 和 t2 线程 在分别获取到 locker1locker2 之后,打印日志;
  2. 打印日志后,两个线程分别休眠 1s ,休眠的目的是为了防止系统随机调度线程(抢占式执行),使得其中一个线程一口气获取到 locker1,locker2两把锁
  3. 在休眠结束后,两个线程分别尝试获取对方已经获取过且没有释放的锁。

143ba47421a64d73a905c8d91f04ad2e.png

我们运行程序,并提供 jconsole 查看 t1,t2 的状态:

5de9724c94e7441c94b52079acd9671c.png

造成死锁之后,t1,t2 都进入阻塞等待的状态,从执行结果的打印日志来看,整个进程被死锁卡住,两个线程各自加第二把锁的时候,jconsolo 的堆栈跟踪也一目了然地表明情况。


  (3)N个线程 M把锁   


  哲学家就餐问题  


0c09f8dc5464488889bb015a40bc037a.png

大部分情况下,上述模型可以很好的运作,但是在一些极端的情况下会造成死锁:
12ed9bc11aeb49babfeff9f3a713658a.png上面的五个线程,都在获取一把锁后,尝试对另一把已经被别的线程获取过的锁,进行加锁,因此所有线程都陷入了阻塞等待的状态,并且五个线程,五把锁之间的等待过程,构成了循环。 

这也使得线程与线程之间,出现了请求保持的情况; 

多个线程,多把锁,出现死锁的情况,如上面五个哲学家谁到凑不齐一双筷子来吃面(造成死锁)的这种情况,是比较典型极端的,当然还有更多种出现死锁的情况;

我们先要处理典型的情况,如刚刚吃面的问题,虽然这种情况可能性很小,但是也不能忽略这种情况。


  2.死锁的四个必要条件  


658141553e2e4ff88a945a6982cff111.png

对于我们在上面描述的,构成死锁的三个场景中,只要涉及 N 个线程,M把锁(N>1 && M>1),并且产生了死锁,原因都是满足了上述的四个必要条件。


  3.如何解决死锁问题  


  • 刚刚构成死锁四个必要条件,锁的互斥与不可抢占,是因为锁的基本特性
  • 要通过解决锁的互斥和不可抢占,来解决死锁问题的做法非常难;
  • 要想打破死锁,避免死锁,我们应该从请求与保持,或者循环等待这两个构成死锁的原因,来寻找突破点。
  • 只要能够解决请求与保持,或者循环等待两个原因中的任意一个,就能够打破死锁~

  (1)避免出现请求和保持  


我们再来分析一下,产生死锁问题的原因,是因为请求保持的代码: 

b7da42e2695346c9b655224fa30e1aaf.png

我们可以发现 ,上述代码中的两个线程,无论是 t1 还是 t2,在进行加锁的代码块中,加锁的方式都是嵌套加锁,这就使得两个线程无法获取对方的锁,又无法解锁,从而双双进入阻塞等待;

换句话说,这种构成死锁情况的原因,就叫做请求保持


   解决方法:  


对于上图的代码,我们要对其进行修改:

  1. 对于t1线程,把 synchronized(locker2) synchronized(locker1)的大括号代码块中取出 
  2. synchronized(locker2) synchronized(locker1) 在 t1 线程中,从嵌套关系变成并列关系
  3. 对于 t2 线程,也作出同样的修改,使得加锁方式从嵌套加锁,修改为并列加锁

6444a3f4b9884a9eacf3527791b8df32.png

 执行结果:

96dc79c9832d40ff84775fcf87af6d1a.png

所以,要想解决请求保持,就不要写出嵌套加锁的代码;但是,在日常开发中,确实会出现代码逻辑,必须要通过嵌套加锁,来完成一些操作;所以嵌套加锁很难避免

因此,我们更通用的打破死锁的做法,就是打破多个线程之间的循环等待关系


  (2)打破多个线程的循环等待关系    


我们把刚刚的并列加锁代码,还原成嵌套加锁:

3a14ee82ceaf497d980d298c073b88a8.png


只要涉及 N 个线程,M把锁(N>1 && M>1),都可以用“哲学家就餐”模型来进行描述

ad1cff50084f4e4d87041b7d57cf52d1.png

只要我们对线程的加锁的顺序做出约定;所有的线程,都按照一定顺序进行加锁,就可以破除循环等待条件,进而打破死锁:

(1)

4202253d7678464fad6eef1164bb2af3.png

(2)

9f9d34c664464b839369a6ddd0fb659f.png

(3)

00e76b618f39440ea61fe92860ae8389.png

(4)

b124ca2ecabf4c51900ca4522facb7c1.png

(5)

cbde44c408c44291901cee12e8b7e25a.png

(6)

11420fe10bec453983db912434556664.png

(7)

此时,看着桌子上所剩不多的 CPU 资源,t1瞬间黑化成邪恶栀子花

86d8c595927f43489cd22ecf27ab89ae.png


通过上述“哲学家就餐”模型,我们能直观的发现,只要规定好加锁顺序,就可以打破多个线程循环等待的关系,进而解决死锁问题。


我们回归代码,来感受一下约定加锁顺序(规定每个线程先获取编号小的锁,再去获取编号大的锁)后,带来的效果;

因为五个线程五把锁的情况,并不容易产生死锁,所以我们就用场景二来演示:

d0185cadf99641788399f15753795b2d.png

  • 对于上述代码,我们约定每个线程都先获取编号小的锁对象;
  • t1 先获取 lcoker1,再获取 locker2,满足约定的规则;
  • t2 先获取 locker2,再获取 locker1,不满足约定的规则,所以需要对 t2 进行修改。

d8837ef2b44244db9a56b72f4c4995bc.png执行结果:

192e1da1b13b4928bea2c4c722518617.png约定加锁顺序后,通过修改后代码的执行结果,我们可以看到, 死锁的问题就被完美的解决了~

 c96f743646e841f8bb30b2d242197f2f.gif

692a78aa0ec843629a817408c97a8b84.gif

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

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

相关文章

【视觉SLAM】1-概述

读书笔记 文章目录 1. 经典视觉SLAM框架2. 数学表述2.1 运动方程2.2 观测方程2.3 问题抽象 1. 经典视觉SLAM框架 传感器信息读取:相机图像、IMU等多源数据;前端视觉里程计(Visual Odometry,VO):估计相机的相…

探索 Python HTTP 的瑞士军刀:Requests 库

文章目录 探索 Python HTTP 的瑞士军刀:Requests 库第一部分:背景介绍第二部分:Requests 库是什么?第三部分:如何安装 Requests 库?第四部分:Requests 库的基本函数使用方法第五部分&#xff1a…

Redisson的可重入锁

初始状态: 表示系统或资源在没有线程持有锁的情况下的状态,任何线程都可以尝试获取锁。 线程 1 获得锁: 线程 1 首次获取了锁并进入受保护的代码区域。 线程 1 再次请求锁: 在持有锁的情况下,线程 1 再次请求锁&a…

基于微信小程序的平安驾校预约平台的设计与实现(源码+LW++远程调试+代码讲解等)

摘 要 互联网发展至今,广泛参与在社会中的方方面面。它让信息都可以通过网络传播,搭配信息管理工具可以很好地为人们提供服务。针对高校教师成果信息管理混乱,出错率高,信息安全性差,劳动强度大,费时费力…

FFmpeg 4.3 音视频-多路H265监控录放C++开发十三.2:avpacket中包含多个 NALU如何解析头部分析

前提: 注意的是:我们这里是从avframe转换成avpacket 后,从avpacket中查看NALU。 在实际开发中,我们有可能是从摄像头中拿到 RGB 或者 PCM,然后将pcm打包成avframe,然后将avframe转换成avpacket&#xff0…

从0学习React(11)

1. 引言 上个星期的工作内容是写IT资产管理的前端页面。其实,尽管我之前有一些前端开发的经验,但并不是很多。这次让我独立完成一个页面的开发,刚开始时我感到无从下手。 2. 初期的困惑和焦虑 我记得在星期一和星期二的时候,那…

BILSTM法律网站用户提问自动分类

项目源码获取方式见文章末尾! 600多个深度学习项目资料,快来加入社群一起学习吧。 《------往期经典推荐------》 项目名称 1.【基于CNN-RNN的影像报告生成】 2.【卫星图像道路检测DeepLabV3Plus模型】 3.【GAN模型实现二次元头像生成】 4.【CNN模型实现…

unity 一个物体随键盘上下左右旋转和前进的脚本

注意:脚本挂在gamaobject 上面 ,操作对象的目标 this.gameObject 为操作对象 using System.Collections; using System.Collections.Generic; using UnityEngine;public class changePosition : MonoBehaviour {//操作对象的目标 this.gameObject 为操…

【论文阅读】Virtual Compiler Is All You Need For Assembly Code Search

阅读笔记:Virtual Compiler Is All You Need For Assembly Code Search 1. 研究背景 逆向工程:逆向工程需要在庞大的二进制文件中快速定位特定功能(例如恶意行为)。传统方法依赖于经验和启发式算法,效率低下。汇编代码搜索:通过自然语言搜索汇编代码功能,能够更高效地处…

Wireshark中的length栏位

注:Ethernet II的最小data length为46,如果小于,会补全到46. 1.指定网卡抓取的,链路为ethernet。 IPv4 Ethernet II 长度为 14 bytes - L1ipv4 header中的length包括header和payload的总长度 - L2wireshark中length表示抓取的pac…

CentOS网络配置

上一篇文章:VMware Workstation安装Centos系统 在CentOS系统中进行网络配置是确保系统能够顺畅接入网络的重要步骤。本文将详细介绍如何配置静态IP地址、网关、DNS等关键网络参数,以帮助需要的人快速掌握CentOS网络配置的基本方法和技巧。通过遵循本文的…

前端搭建低代码平台,微前端如何选型?

目录 背景 一、微前端是什么? 二、三大特性 三、现有微前端解决方案 1、iframe 2、Web Components 3、ESM 4、EMP 5、Fronts 6、无界(文档) 7、qiankun 四、我们选择的方案 引入qiankun并使用(src外层作为主应用) 主应…

CSS:怎么把网站都变成灰色

当大家看到全站的内容都变成了灰色,包括按钮、图片等等。这时候我们可能会好奇这是怎么做到的呢? 有人会以为所有的内容都统一换了一个 CSS 样式,图片也全换成灰色的了,按钮等样式也统一换成了灰色样式。但你想想这个成本也太高了…

基于Spring Boot的计算机课程管理:工程认证的实践

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统,它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等,非常…

flinkOnYarn并配置prometheus+grafana监控告警

flinkOnYarn并配置prometheusgrafana监控告警 一、相关服务版本: flink版本:1.17.2 pushgateway版本:1.10.0 prometheus版本:3.0.0 grafana-v11.3.0参考了网上的多个文档以及学习某硅谷的视频,总结了一下文档&#x…

在esxi8.0中安装黑群晖的过程记录及小问题处理

问题记录 1.某种原因在网页中安装系统后,发现synology搜出来的设备还是169的地址,但是点击设置需要输入管理员账号密码才能设置ip,试了一下,账号输入admin,密码留空正常设置。 2.晚上试了一下,在全新的esxi…

基于微信小程序的公务员考试学习平台的设计与实现,LW+源码+讲解

摘 要 小程序公考学习平台使用Java语言进行编码,使用Mysql创建数据表保存本系统产生的数据。系统可以提供信息显示和相应服务,其管理小程序公考学习平台信息,查看小程序公考学习平台信息,管理小程序公考学习平台。 总之&#x…

深度学习之pytorch常见的学习率绘制

文章目录 0. Scope1. StepLR2. MultiStepLR3. ExponentialLR4. CosineAnnealingLR5. ReduceLROnPlateau6. CyclicLR7. OneCycleLR小结参考文献 https://blog.csdn.net/coldasice342/article/details/143435848 0. Scope 在深度学习中,学习率(Learning R…

2024 年(第 7 届)“泰迪杯”数据分析技能赛B 题 特殊医学用途配方食品数据分析 完整代码 结果 可视化分享

一、背景特殊医学用途配方食品简称特医食品,是指为满足进食受限、消化吸收障碍、代谢素乱或者特定疾病状态人群对营养素或者膳食的特殊需要,专门加工配置而成的配方食品,包括0月龄至12月龄的特殊医学用途婴儿配方食品和适用于1岁以上的特殊医…

数据产品:深度探索与案例剖析

​在当今数字化时代,数据产品正逐渐成为各行业发展的关键驱动力。让我们深入了解数据产品的分类与特点,以及通过典型案例分析,感受数据产品的强大魅力。 首先,数据产品主要分为报表型、分析型、平台型等不同类别。 报表型数据产品…