当前位置: 首页 > news >正文

【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 线程 在分别获取到 locker1,locker2 之后,打印日志;
  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

 

 

相关文章:

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

目录 死锁 1.构成死锁的场景 (1) 一个线程一把锁 问题描述 解决方案(可重入锁) (2) 两个线程两把锁 问题描述 (3)N个线程 M把锁 哲学家就餐问题 2.死锁的四个必要条件 3.如何解决死锁问题 (1)避免出现请求和保持 (2)打破多个线程的循环等待关系 死锁…...

mapper.xml 使用大于号、小于号示例

<mapper namespace"com.example.EmployeeMapper"><!-- 更新employee_absent_resign_statistics表中的pre_work_date --><update id"updatePreWorkDate"><![CDATA[UPDATE employee e1JOIN employee e2ON e2.statistics_date < e1.s…...

深入了解决策树:机器学习中的经典算法

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…...

Flutter鸿蒙next 的 Sliver 实现自定义滚动效果

Flutter 提供了一些非常强大的滚动组件&#xff0c;如 ListView、GridView 等&#xff0c;它们可以在滑动时自动处理内容的显示和滚动。但当我们需要更复杂的滚动效果时&#xff0c;Sliver 组件便是一个强大的工具。通过自定义 Sliver&#xff0c;我们可以实现高度定制化的滚动…...

杨中科 .Net Core 笔记 DI 依赖注入

提到依赖不得不提到&#xff0c;控制反转&#xff08;Inversion of Control,IOC&#xff09;这个概念&#xff0c;简单的来讲就是将控制对象的权限交给框架&#xff0c;不再手动完成。IOC实现方式有2种&#xff1a; 1、服务定位器&#xff08;ServiceLocator&#xff09;,主动…...

【RocketMQ】无法访问此网站 http://XXX:10080/ ERR_UNSAFE_PORT

安装完rocketmq-dashboard。打开浏览器访问地址。 问题提示&#xff1a; 无法访问此网站 网址为 http://192.168.22.197:10080/ 的网页可能暂时无法连接&#xff0c;或者它已永久性地移动到了新网址。 ERR_UNSAFE_PORT ‌无法访问10080端口的网站通常是由于Chrome浏览器的安…...

pipreqs:快速准确生成当前项目的requirements.txt,还有和freeze的对比

大家好&#xff0c;这里是程序员晚枫。 今天给大家推荐一个快速生成requirements.txt的小工具&#xff1a;pipreqs。 什么是requirements.txt&#xff1f; 我们在开发Python项目的时候&#xff0c;需要用到requirements.txt来管理项目中使用的第三方库。 当我们把项目部署到…...

Spark 中的 RDD 分区的设定规则与高阶函数、Lambda 表达式详解

Spark 的介绍与搭建&#xff1a;从理论到实践_spark环境搭建-CSDN博客 Spark 的Standalone集群环境安装与测试-CSDN博客 PySpark 本地开发环境搭建与实践-CSDN博客 Spark 程序开发与提交&#xff1a;本地与集群模式全解析-CSDN博客 Spark on YARN&#xff1a;Spark集群模式…...

redis十大数据类型

文章目录 一、redis字符串&#xff08;String&#xff09;set key value同时获取或设置多个键值获取指定区间范围内的值数字增减获取字符串长度和内容追加分布式锁getset&#xff08;先get再set&#xff09; 二、redis列表&#xff08;List&#xff09;通过索引获取列表中的元素…...

国内AI工具复现GPTs效果详解

国内AI工具复现GPTs效果详解 引言 近年来&#xff0c;随着人工智能技术的飞速发展&#xff0c;大型语言模型&#xff08;LLM&#xff09;逐渐成为研究和应用的热点。GPTs&#xff08;Generative Pre-trained Transformer&#xff09;系列模型&#xff0c;特别是GPT-4的推出&a…...

【学习笔记】SAP ABAP——OPEN SQL(一)【INTO语句】

【INTO语句】 结构体插入(插入一条语句时) SELECT...INTO [CORRESPONDING FIELDS OF] <wa> FROM <db> WHERE <condition>.内表插入(插入多条语句时) SELECT...INTO|APPENDING [CORRESPONDING FIELDS OF] TABLE <itab>FROM <db> WHERE <con…...

vscode使用之vscode-server离线安装

最近因为想要使用AI工具开始使用vscode&#xff0c;但是在内网使用vscode通过SSH连接虚拟机的centos远程目录却出现了问题&#xff0c;始终连不上&#xff0c;查看原因是centos没有安装vscode-server&#xff0c;网上找各个教程离线安装vscode-code除了浪费时间没有任何收获&am…...

字符编码和字符集

1. 字符编码和字符集 1.1. 字符编码 编码&#xff1a;字符 –>字节解码&#xff1a;字节 –>字符字符编码Character Encoding : 就是一套自然语言的字符与二进制数之间的对应规则。 1.2. 字符集 字符集 Charset&#xff1a;是一个系统支持的所有字符的集合&#xff0…...

【WRF理论第七期】WPS预处理

【WRF理论第七期】WPS预处理 运行WPS&#xff08;Running the WPS&#xff09;步骤1&#xff1a;Define model domains with geogrid步骤2&#xff1a;Extracting meteorological fields from GRIB files with ungrib步骤3&#xff1a;Horizontally interpolating meteorologic…...

Flutter鸿蒙next中的按钮封装:自定义样式与交互

在Flutter应用开发中&#xff0c;按钮是用户界面中不可或缺的组件之一。它不仅用于触发事件&#xff0c;还可以作为视觉元素增强用户体验。Flutter提供了多种按钮组件&#xff0c;如ElevatedButton、TextButton、OutlinedButton等&#xff0c;但有时这些预制的按钮样式无法满足…...

代码随想录算法训练营Day57 | 卡玛网 101.孤岛的总面积、卡玛网 102.沉没孤岛、卡玛网 103. 水流问题、卡玛网 104.建造最大岛屿

目录 卡玛网 101.孤岛的总面积 卡玛网 102.沉没孤岛 卡玛网 103. 水流问题 卡玛网 104.建造最大岛屿 卡玛网 101.孤岛的总面积 题目 101. 孤岛的总面积 思路 代码随想录&#xff1a;101.孤岛的总面积 重点&#xff1a; 首先遍历图的四条边&#xff0c;把其中的陆地及…...

美团代付微信小程序系统 read.php 任意文件读取漏洞复现

0x01 产品简介 美团代付微信小程序系统是美团点评旗下的一款基于微信小程序技术开发的应用程序功能之一,它允许用户方便快捷地请求他人为自己支付订单费用。随着移动支付的普及和微信小程序的广泛应用,美团作为中国领先的本地生活服务平台,推出了代付功能,以满足用户多样化…...

Windows安装tensorflow的GPU版本

前言 首先本文讨论的是windows系统&#xff0c;显卡是英伟达&#xff08;invida&#xff09;如何安装tensorflow-gpu。一共需要安装tensorflow-gpu、cuDNN、CUDA三个东西。其中CUDA是显卡的驱动库&#xff0c;cuDNN是深度学习加速库。 安装开始前&#xff0c;首先需要安装好c…...

2021-04-22 51单片机玩转点阵

理论就不赘述了,网络上多得很,直接从仿真软件感性上操作认识点阵,首先打开ISIS仿真软件,放置一个点阵和电源与地线就可以开始了;由点阵任何一脚连线到地线,另一边对应的引脚就连接到电源,如图:点击运行看是否点亮?看到蓝色与红色的点表示电源正常但是没有任何亮点,这时对调一下…...

lua入门教程:数字

在Lua中&#xff0c;数字&#xff08;number&#xff09;是一种基本数据类型&#xff0c;用于表示数值。以下是对Lua中数字的详细教程&#xff1a; 一、数字类型概述 Lua中的数字遵循IEEE 754双精度浮点标准&#xff0c;可以表示非常大的正数和负数&#xff0c;以及非常小的正…...

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...

AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

谷歌浏览器插件

项目中有时候会用到插件 sync-cookie-extension1.0.0&#xff1a;开发环境同步测试 cookie 至 localhost&#xff0c;便于本地请求服务携带 cookie 参考地址&#xff1a;https://juejin.cn/post/7139354571712757767 里面有源码下载下来&#xff0c;加在到扩展即可使用FeHelp…...

Android Wi-Fi 连接失败日志分析

1. Android wifi 关键日志总结 (1) Wi-Fi 断开 (CTRL-EVENT-DISCONNECTED reason3) 日志相关部分&#xff1a; 06-05 10:48:40.987 943 943 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid44:9b:c1:57:a8:90 reason3 locally_generated1解析&#xff1a; CTR…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘

美国西海岸的夏天&#xff0c;再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至&#xff0c;这不仅是开发者的盛宴&#xff0c;更是全球数亿苹果用户翘首以盼的科技春晚。今年&#xff0c;苹果依旧为我们带来了全家桶式的系统更新&#xff0c;包括 iOS 26、iPadOS 26…...

【OSG学习笔记】Day 18: 碰撞检测与物理交互

物理引擎&#xff08;Physics Engine&#xff09; 物理引擎 是一种通过计算机模拟物理规律&#xff08;如力学、碰撞、重力、流体动力学等&#xff09;的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互&#xff0c;广泛应用于 游戏开发、动画制作、虚…...

React hook之useRef

React useRef 详解 useRef 是 React 提供的一个 Hook&#xff0c;用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途&#xff0c;下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...

在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:

在 HarmonyOS 应用开发中&#xff0c;手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力&#xff0c;既支持点击、长按、拖拽等基础单一手势的精细控制&#xff0c;也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档&#xff0c…...

Golang dig框架与GraphQL的完美结合

将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用&#xff0c;可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器&#xff0c;能够帮助开发者更好地管理复杂的依赖关系&#xff0c;而 GraphQL 则是一种用于 API 的查询语言&#xff0c;能够提…...

MMaDA: Multimodal Large Diffusion Language Models

CODE &#xff1a; https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA&#xff0c;它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构&#xf…...