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


目录
死锁
1.构成死锁的场景
(1) 一个线程一把锁
问题描述
解决方案(可重入锁)
(2) 两个线程两把锁
问题描述
(3)N个线程 M把锁
哲学家就餐问题
2.死锁的四个必要条件
3.如何解决死锁问题
(1)避免出现请求和保持
(2)打破多个线程的循环等待关系
死锁
1.构成死锁的场景
(1) 一个线程一把锁
问题描述
我们先来看下面的代码:
看起来是两次对同一个引用的加锁,是没有必要的,但是在我们日常学习和工作中,很容易就会写出上述的对一个锁对象进行两次或者多次加锁的操作,此时会出现如下情况:

- 如果第一次加锁要解锁,就必须得先执行完{}中的代码块,就必须进行第二次加锁;
- 但是第二次加锁时,发现锁对象locker还未被解锁,第二次加锁因此进入阻塞等待的状态,所以第一次加锁的操作无法执行到解锁的位置;
- 上面的这种情况,就被称为“死锁”(dead lock);死锁是一个非常严重的 bug ,一旦出现,整个线程都会被卡住。
上面的代码虽然会造成死锁,但是我们不太容易写出上面的代码;但是一旦方法调用的层次比较深,就容易出现对同一对象进行多次加锁的情况。 我们再来分析下面的代码:

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

解决方案(可重入锁)
为了解决上述代码出现的死锁问题, Java 的 synchronized 就引入了可重入的概念;
当 t线程 对 locker对象 加锁成功之后,后续 t 再次针对 locker 进行加锁,不会触发阻塞,而是直接往下走,因为当前 locker 就是被 t 持有~~
但是,如果是其他线程尝试加锁,就会正常进入阻塞等待的状态;
- 如果发现是同一个锁持有者的线程,则跳过加锁环节;
- 如果是不同的锁持有者,才会进入阻塞等待。
我们运行刚刚所写的代码,发现程序是可以正常执行的:
- 理论上,程序会被上死锁,但是当我们正在运行程序时,会发现程序依旧可以正常执行,输出结果也正确;
- 这样的原因是因为 synchronized 的可重入性,解决了当前情况(一个线程针对同一个锁对象进行多次加锁)造成的死锁的问题,哪怕我们再锁三四层,synchronized 的可重入性都会解决该问题。
可重入锁只能针对 一个线程多次对锁对象进行加锁 的情况,如果是其他情况造成的死锁,则无法通过可重入锁解决。
面试官的问题:
如何自己实现一个可重入锁?
- 在锁内部记录当前是哪个线程持有的锁,后续每次加锁,都进行判定
- 通过计数器,记录当前加锁的次数,从而确定何时真正进行解锁.
(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();}
}
代码逻辑:
- t1 线程 和 t2 线程 在分别获取到 locker1,locker2 之后,打印日志;
- 打印日志后,两个线程分别休眠 1s ,休眠的目的是为了防止系统随机调度线程(抢占式执行),使得其中一个线程一口气获取到 locker1,locker2两把锁;
- 在休眠结束后,两个线程分别尝试获取对方已经获取过且没有释放的锁。
我们运行程序,并提供 jconsole 查看 t1,t2 的状态:

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

大部分情况下,上述模型可以很好的运作,但是在一些极端的情况下会造成死锁:
上面的五个线程,都在获取一把锁后,尝试对另一把已经被别的线程获取过的锁,进行加锁,因此所有线程都陷入了阻塞等待的状态,并且五个线程,五把锁之间的等待过程,构成了循环。
这也使得线程与线程之间,出现了请求保持的情况;
多个线程,多把锁,出现死锁的情况,如上面吃面造成死锁的这种情况,是比较典型极端的,当然还有更多种出现死锁的情况;
我们先要处理典型的情况,如刚刚吃面的问题,虽然这种情况可能性很小,但是也不能忽略这种情况。
2.死锁的四个必要条件
对于我们在上面描述的,构成死锁的三个场景中,只要涉及 N 个线程,M把锁(N>1 && M>1),并且产生了死锁,原因都是满足了上述的四个必要条件。
3.如何解决死锁问题
- 刚刚构成死锁四个必要条件,锁的互斥与不可抢占,是因为锁的基本特性;
- 要通过解决锁的互斥和不可抢占,来解决死锁问题的做法非常难;
- 要想打破死锁,避免死锁,我们应该从请求与保持,或者循环等待这两个构成死锁的原因,来寻找突破点。
- 只要能够解决请求与保持,或者循环等待两个原因中的任意一个,就能够打破死锁~
(1)避免出现请求和保持
我们再来分析一下,产生死锁问题的原因,是因为请求保持的代码:

我们可以发现 ,上述代码中的两个线程,无论是 t1 还是 t2,在进行加锁的代码块中,加锁的方式都是嵌套加锁,这就使得两个线程无法获取对方的锁,又无法解锁,从而双双进入阻塞等待;
换句话说,这种构成死锁情况的原因,就叫做请求保持;
解决方法:
对于上图的代码,我们要对其进行修改:
- 对于t1线程,把synchronized(locker2) 从 synchronized(locker1)的大括号代码块中取出
- synchronized(locker2) 和 synchronized(locker1) 在 t1 线程中,从嵌套关系变成并列关系;
- 对于 t2 线程,也作出同样的修改,使得加锁方式从嵌套加锁,修改为并列加锁:

执行结果:

所以,要想解决请求保持,就不要写出嵌套加锁的代码;但是,在日常开发中,确实会出现代码逻辑,必须要通过嵌套加锁,来完成一些操作;所以嵌套加锁很难避免;
因此,我们更通用的打破死锁的做法,就是打破多个线程之间的循环等待关系。
(2)打破多个线程的循环等待关系
我们把刚刚的并列加锁代码,还原成嵌套加锁:

只要涉及 N 个线程,M把锁(N>1 && M>1),都可以用“哲学家就餐”模型来进行描述:
只要我们对线程的加锁的顺序做出约定;所有的线程,都按照一定顺序进行加锁,就可以破除循环等待条件,进而打破死锁:
(1)
(2)
(3)
(4)
(5)
(6)
(7)
此时,看着桌子上所剩不多的 CPU 资源,t1瞬间黑化成邪恶栀子花
通过上述“哲学家就餐”模型,我们能直观的发现,只要规定好加锁顺序,就可以打破多个线程循环等待的关系,进而解决死锁问题。
我们回归代码,来感受一下约定加锁顺序(规定每个线程先获取编号小的锁,再去获取编号大的锁)后,带来的效果;
因为五个线程五把锁的情况,并不容易产生死锁,所以我们就用场景二来演示:

- 对于上述代码,我们约定每个线程都先获取编号小的锁对象;
- t1 先获取 lcoker1,再获取 locker2,满足约定的规则;
- t2 先获取 locker2,再获取 locker1,不满足约定的规则,所以需要对 t2 进行修改。
执行结果:
约定加锁顺序后,通过修改后代码的执行结果,我们可以看到, 死锁的问题就被完美的解决了~

相关文章:
【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…...
深入了解决策树:机器学习中的经典算法
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…...
Flutter鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了一些非常强大的滚动组件,如 ListView、GridView 等,它们可以在滑动时自动处理内容的显示和滚动。但当我们需要更复杂的滚动效果时,Sliver 组件便是一个强大的工具。通过自定义 Sliver,我们可以实现高度定制化的滚动…...
杨中科 .Net Core 笔记 DI 依赖注入
提到依赖不得不提到,控制反转(Inversion of Control,IOC)这个概念,简单的来讲就是将控制对象的权限交给框架,不再手动完成。IOC实现方式有2种: 1、服务定位器(ServiceLocator),主动…...
【RocketMQ】无法访问此网站 http://XXX:10080/ ERR_UNSAFE_PORT
安装完rocketmq-dashboard。打开浏览器访问地址。 问题提示: 无法访问此网站 网址为 http://192.168.22.197:10080/ 的网页可能暂时无法连接,或者它已永久性地移动到了新网址。 ERR_UNSAFE_PORT 无法访问10080端口的网站通常是由于Chrome浏览器的安…...
pipreqs:快速准确生成当前项目的requirements.txt,还有和freeze的对比
大家好,这里是程序员晚枫。 今天给大家推荐一个快速生成requirements.txt的小工具:pipreqs。 什么是requirements.txt? 我们在开发Python项目的时候,需要用到requirements.txt来管理项目中使用的第三方库。 当我们把项目部署到…...
Spark 中的 RDD 分区的设定规则与高阶函数、Lambda 表达式详解
Spark 的介绍与搭建:从理论到实践_spark环境搭建-CSDN博客 Spark 的Standalone集群环境安装与测试-CSDN博客 PySpark 本地开发环境搭建与实践-CSDN博客 Spark 程序开发与提交:本地与集群模式全解析-CSDN博客 Spark on YARN:Spark集群模式…...
redis十大数据类型
文章目录 一、redis字符串(String)set key value同时获取或设置多个键值获取指定区间范围内的值数字增减获取字符串长度和内容追加分布式锁getset(先get再set) 二、redis列表(List)通过索引获取列表中的元素…...
国内AI工具复现GPTs效果详解
国内AI工具复现GPTs效果详解 引言 近年来,随着人工智能技术的飞速发展,大型语言模型(LLM)逐渐成为研究和应用的热点。GPTs(Generative Pre-trained Transformer)系列模型,特别是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,但是在内网使用vscode通过SSH连接虚拟机的centos远程目录却出现了问题,始终连不上,查看原因是centos没有安装vscode-server,网上找各个教程离线安装vscode-code除了浪费时间没有任何收获&am…...
字符编码和字符集
1. 字符编码和字符集 1.1. 字符编码 编码:字符 –>字节解码:字节 –>字符字符编码Character Encoding : 就是一套自然语言的字符与二进制数之间的对应规则。 1.2. 字符集 字符集 Charset:是一个系统支持的所有字符的集合࿰…...
【WRF理论第七期】WPS预处理
【WRF理论第七期】WPS预处理 运行WPS(Running the WPS)步骤1:Define model domains with geogrid步骤2:Extracting meteorological fields from GRIB files with ungrib步骤3:Horizontally interpolating meteorologic…...
Flutter鸿蒙next中的按钮封装:自定义样式与交互
在Flutter应用开发中,按钮是用户界面中不可或缺的组件之一。它不仅用于触发事件,还可以作为视觉元素增强用户体验。Flutter提供了多种按钮组件,如ElevatedButton、TextButton、OutlinedButton等,但有时这些预制的按钮样式无法满足…...
代码随想录算法训练营Day57 | 卡玛网 101.孤岛的总面积、卡玛网 102.沉没孤岛、卡玛网 103. 水流问题、卡玛网 104.建造最大岛屿
目录 卡玛网 101.孤岛的总面积 卡玛网 102.沉没孤岛 卡玛网 103. 水流问题 卡玛网 104.建造最大岛屿 卡玛网 101.孤岛的总面积 题目 101. 孤岛的总面积 思路 代码随想录:101.孤岛的总面积 重点: 首先遍历图的四条边,把其中的陆地及…...
美团代付微信小程序系统 read.php 任意文件读取漏洞复现
0x01 产品简介 美团代付微信小程序系统是美团点评旗下的一款基于微信小程序技术开发的应用程序功能之一,它允许用户方便快捷地请求他人为自己支付订单费用。随着移动支付的普及和微信小程序的广泛应用,美团作为中国领先的本地生活服务平台,推出了代付功能,以满足用户多样化…...
Windows安装tensorflow的GPU版本
前言 首先本文讨论的是windows系统,显卡是英伟达(invida)如何安装tensorflow-gpu。一共需要安装tensorflow-gpu、cuDNN、CUDA三个东西。其中CUDA是显卡的驱动库,cuDNN是深度学习加速库。 安装开始前,首先需要安装好c…...
2021-04-22 51单片机玩转点阵
理论就不赘述了,网络上多得很,直接从仿真软件感性上操作认识点阵,首先打开ISIS仿真软件,放置一个点阵和电源与地线就可以开始了;由点阵任何一脚连线到地线,另一边对应的引脚就连接到电源,如图:点击运行看是否点亮?看到蓝色与红色的点表示电源正常但是没有任何亮点,这时对调一下…...
lua入门教程:数字
在Lua中,数字(number)是一种基本数据类型,用于表示数值。以下是对Lua中数字的详细教程: 一、数字类型概述 Lua中的数字遵循IEEE 754双精度浮点标准,可以表示非常大的正数和负数,以及非常小的正…...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...
【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)
可以使用Sqliteviz这个网站免费编写sql语句,它能够让用户直接在浏览器内练习SQL的语法,不需要安装任何软件。 链接如下: sqliteviz 注意: 在转写SQL语法时,关键字之间有一个特定的顺序,这个顺序会影响到…...
srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
镜像里切换为普通用户
如果你登录远程虚拟机默认就是 root 用户,但你不希望用 root 权限运行 ns-3(这是对的,ns3 工具会拒绝 root),你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案:创建非 roo…...
CMake 从 GitHub 下载第三方库并使用
有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...
CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云
目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南 在数字化营销时代,邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天,我们将深入解析邮件打开率、网站可用性、页面参与时…...
学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”
2025年#高考 将在近日拉开帷幕,#AI 监考一度冲上热搜。当AI深度融入高考,#时间同步 不再是辅助功能,而是决定AI监考系统成败的“生命线”。 AI亮相2025高考,40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕,江西、…...
QT3D学习笔记——圆台、圆锥
类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体(对象或容器)QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质(定义颜色、反光等)QFirstPersonC…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...














