Java面试必问:死锁(多线程死锁+数据库死锁)

article/2025/8/26 18:07:00

死锁

接下来从几个方面介绍:

  1. 多线程死锁
  2. 多线程死锁解决办法
  3. 数据库死锁
  4. 数据库死锁解决办法

多线程死锁是怎么造成的?

  1. 多线程锁定同一资源会造成死锁
  2. 线程池中的任务使用当前线程池也可能出现死锁
参考连接:
https://blog.csdn.net/qq_35064774/article/details/51793656

情况一: 死锁是两个或多个线程互相等待对方所有用的资源情形:现在有线程1和线程2。线程1执行过程中,先锁定了对象a,然后需要再锁定b才能继续执行代码;而线程2正巧相反,先锁定了b,需要再锁定a才能继续执行代码。这时,两个线程都等着对方解锁,才能继续执行,这时,两个线程就进入等待状态,最终不会有线程执行。这就变成了死锁。

接下来是代码实例:

class DeadLock implements Runnable{boolean lockFormer;static Object o1 = new Object();static Object o2 = new Object();DeadLock(boolean lockFormer){this.lockFormer = lockFormer;}@Overridepublic void run() {if (this.lockFormer){synchronized(o1){try{Thread.sleep(500);System.out.println("线程1");}catch (Exception e){e.printStackTrace();}synchronized (o2){System.out.println("lok");}}}else {synchronized (o2){try{Thread.sleep(500);System.out.println("线程2");}catch (Exception e){e.printStackTrace();}synchronized (o1){System.out.println("lok");}}}}
}

尽量避免加多个锁,避免死锁。

参考连接:
http://www.importnew.com/30277.html#comment-795471
https://www.cnblogs.com/caoshenglu/p/9461567.html

情况二: 线程池自己引发的死锁
单线程使用不当引发死锁,实现代码:

@Slf4jclass DeadLock2 {public static void main(String[] args) {ExecutorService pool = Executors.newFixedThreadPool(10);pool.submit(() -> {try{log.info("First");pool.submit(() -> log.info("second")).get();log.info("third");}catch (InterruptedException | ExecutionException e){log.error("Error",e);}});System.out.println("process is over");}
}

看起来没有什么问题-所有信息都按预期输出:

process is over
15:46:07.470 [pool-1-thread-1] INFO xiaowang.org.prictice.DeadLock2 - First
15:46:07.474 [pool-1-thread-2] INFO xiaowang.org.prictice.DeadLock2 - second
15:46:07.474 [pool-1-thread-1] INFO xiaowang.org.prictice.DeadLock2 - third

注意我们用 get() 阻塞线程,在显示“Third”之前必须等待内部线程(Runnable)运行完成。这是个大坑!等待内部任务完成意味着需要从线程池额外获取一个线程来执行任务。然而,我们已经使用到了一个线程,所以内部任务在获取到第二个线程前将一直阻塞。当前我们的线程池足够大,运行没问题。让我们稍微改变一下代码,将线程池缩减到只有一个线程,另外关键的一点是我们移除 get() 方法:

@Slf4jclass DeadLock2 {public static void main(String[] args) {ExecutorService pool = Executors.newFixedThreadPool(10);pool.submit(() -> {log.info("First");pool.submit(() -> log.info("second"));log.info("third");});System.out.println("process is over");}
}

代码运行正常,只是有点乱:

15:51:06.334 [pool-1-thread-1] INFO xiaowang.org.prictice.DeadLock2 - First
15:51:06.339 [pool-1-thread-2] INFO xiaowang.org.prictice.DeadLock2 - second
15:51:06.339 [pool-1-thread-1] INFO xiaowang.org.prictice.DeadLock2 - third

顺序的改变完全在预料之内,没有涉及线程间的竞态条件(事实上我们只有一个线程)。仔细分析一下发生了什么:我们向线程池提交了一个新任务(打印“Second”的任务),但这次我们不需要等待这个任务完成。因为线程池中唯一的线程被打印“First”和“Third”的任务占用,所以这个外层任务继续执行,并打印“Third”。当这个任务完成时,将单个线程释放回线程池,内部任务最终开始执行,并打印“Second”。那么死锁在哪里?来试试在内部任务里加上 get() 方法:

只有一个线程,
ExecutorService pool = Executors.newFixedThreadPool(1)

@Slf4jclass DeadLock2 {public static void main(String[] args) {ExecutorService pool = Executors.newFixedThreadPool(1);pool.submit(() -> {try{log.info("First");pool.submit(() -> log.info("second")).get();log.info("third");}catch (InterruptedException | ExecutionException e){log.error("Error",e);}});System.out.println("process is over");}
}

死锁出现了!我们来一步一步分析:

  • 打印“First”的任务被提交到只有一个线程的线程池
  • 任务开始执行并打印“First”
  • 我们向线程池提交了一个内部任务,来打印“Second”
  • 内部任务进入等待任务队列。没有可用线程因为唯一的线程正在被占用
  • 我们阻塞住并等待内部任务执行结果。不幸的是,我们等待内部任务的同时也在占用着唯一的可用线程
  • get() 方法无限等待,无法获取线程
  • 死锁

多线程死锁解决办法

参考地址:
http://ifeve.com/deadlock-prevention/
https://mp.weixin.qq.com/s/BVGtDDCa7yjtfJJPNKOC_g

方法一: 加锁顺序
当索格线程需要相同的锁,但按照不同的顺序加锁,死锁就很容易发生。
如果能确保所有的线程都是按照相同的顺序获取锁,那么死锁就不会发生,实例如下:

Thread 1:lock ALock BThread 2 :wait for Alock C (when A locked)Thread 3:wait for Await for Bwait for C

如果一个线程(比如线程3)需要一些锁,那么必须按照确定的顺序获取锁。他只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。

例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C。因为线程1已经拥有了锁A,所以线程2和3需要等到锁A被释放。然后他们尝试对B和C加锁之前,必须成功的对A加锁。

按顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁,但总有些时候是无法预知的。

方法二: 加锁限时
另外一个避免死锁的方法,尝试获取锁时候加一个超时时间,这也意味着在尝试获取的过程中,若超过了这个时限,该线程则放弃对该锁的请求。若一个线程没有在给定的时间内获取到所需要的锁,则进行回退并释放所有以获得的锁,然后再等待一段随机时间再尝试。这段随机的等待时间,让其他线程有机会尝试获取相同的这些锁,并且让该应用再没有获得锁的时候可以继续进行。
实例如下:

Thread 1 locks A
Thread 2 locks BThread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blockedThread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在以上的例子中,线程2比线程1早200毫秒经行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁)。

需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。

此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。
(译者注:超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。)

这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。后续的Java并发系列会涵盖自定义锁的内容。

方法三: 死锁检测
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。

我们在命令行中敲入jconsole命令,会自动弹出以下对话框,选择进程1362,并点击“链接”
在这里插入图片描述
进入所检测的进程后,选择“线程”选项卡,并点击“检测死锁”
在这里插入图片描述

数据库死锁

参考连接:
https://juejin.im/entry/57e7685abf22ec00586ed574

1. innodb隔离级别、索引与锁

假设我们有一张表(msg),里面有3个字段。假设id是主键,token是非唯一索引,message没有索引。

id:bigint           token:varchar(30)                               message:varchar(4096)

innodb对于主键使用密集索引,这是一种数据存储的方式,表数据是和主键一起存储,主键索引的叶子结点存储行数据。对于普通索引,其叶子结点存储的是主键值。

Alt
下面分析索引和锁的关系(在RC级别下):

  1. delete from msg where id = 2;

    由于id是主键,因此直接锁住整行记录即可。

Alt
2. delete from msg where token = ‘cvs’;
由于token是二级索引,因此先锁住二级索引(两行),接着会锁住相应主键对应的记录;
Alt
3. delete from msg where message = ‘订单号是多少’;
message没有索引,所以走的是全表扫描过滤。这时表上的各个记录都能将添加上X锁。
Atl

2. 锁与隔离级别的关系

数据库的事务隔离级别:

  1. 未提交读(read uncommitted)
  2. 已提交读(read committed):能读到已经提交的数据。
  3. 可重复读(repeatable read):在同一个事务内查询都是事务开始时刻一致的,InnoDB默认级别.
  4. 串行化(Serializable)

我们较常用的是RC和RR.
如下图所示,事务A在第一次查询时得到1条记录,在第二次执行相同查询时却得到两条记录。从事务A角度上看是见鬼了!这就是幻读,RC级别下尽管加了行锁,但还是避免不了幻读。
Alt
innodb的RR隔离级别可以避免幻读发生,怎么实现?当然需要借助于锁了!

为了解决幻读问题,innodb引入了gap锁。

在事务A执行:update msg set message=‘订单’ where token=‘asd’;

innodb首先会和RC级别一样,给索引上的记录添加上X锁,此外,还在非唯一索引’asd’与相邻两个索引的区间加上锁。

这样,当事务B在执行insert into msg values (null,‘asd’,’hello’); commit;时,会首先检查这个区间是否被锁上,如果被锁上,则不能立即执行,需要等待该gap锁被释放。这样就能避免幻读问题。
Alt

3. 死锁成因

情况一: 不同的表相同记录行锁冲突
这种情况很好理解,事务A和事务B操作两张表,但是出现循环等待情况。
Atl
情况二: 相同表记录行锁冲突
这种情况比较常见,之前遇到两个job在执行数据批量更新时,jobA处理的的id列表为[1,2,3,4],而job处理的id列表为[8,9,10,4,2],这样就造成了死锁。
alt
情况三: 不同索引锁冲突
这种情况比较隐晦,事务A在执行时,除了在二级索引加锁外,还会在聚簇索引上加锁,在聚簇索引上加锁的顺序是[1,4,2,3,5],而事务B执行时,只在聚簇索引上加锁,加锁顺序是[1,2,3,4,5],这样就造成了死锁的可能性。
alt
情况四: gap锁冲突
innodb在RR级别下,如下的情况也会产生死锁,比较隐晦。不清楚的同学可以自行根据上节的gap锁原理分析下。
alt

如何尽可能避免数据库死锁

  1. 以固定的顺序访问表和行。比如对第2节两个job批量更新的情形,简单方法是对id列表先排序,后执行,这样就避免了交叉等待锁的情形;又比如对于3.1节的情形,将两个事务的sql顺序调整为一致,也能避免死锁。
  2. 大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。
  3. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。
  4. 降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
  5. 为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增大。

http://chatgpt.dhexx.cn/article/3yjoZiRB.shtml

相关文章

MySQL数据库死锁了,该怎么办?一文全解最新教程

文章目录 正文死锁的发生为什么会产生死锁?Insert 语句是怎么加行级锁的?1、记录之间加有间隙锁2、遇到唯一键冲突 如何避免死锁? 之前分享过 MySQL 死锁的文章,然后很多读者对「插入意向锁」认识很迷糊。 大家误以为「插入意向锁…

5 分钟理解数据库死锁

图片来源:网络 文章目录 死锁是如何产生的?如何解决并避免死锁总结 🍺知人者智,自知者明。胜人者有力,胜己者强。知足者富,强行者有志。不失其所者久,死而不亡者寿。——老子 大家好&#xff01…

数据库死锁场景

场景一: 单一线程多次进入子事务发生死锁 问题: 线上问题发生了死锁,但通过死锁日志发现一直在等待查询结果。我们使用的数据库是PGsql,默认的隔离级别是“读已提交”,按理来说查询不会加锁,导致一度被带偏…

数据库常见死锁原因及处理

目录 前言什么是死锁死锁产生的四个必要条件 1. 表锁死锁死锁场景解决方案建议 2. 行锁死锁2.1 两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁死锁场景解决方案 2.2 共享锁转换为排他锁死锁场景解决方案 3. INSERT ... ON DUPLICATE KEY UPDATE…

数据库死锁分析与解决

一、死锁的表现 1、错误信息是:事务(进程 ID)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品。请重新运行该事务。 2、错误信息是:事务(进程 ID )与另一个进程被死锁在 锁 | 通信缓冲区 资源上,并且已被选作死锁牺牲品。请重新运行该事务。 二、…

数字 IC 技能拓展(1)Xilinx_Vivado_SDK_2019.1 安装详细教程

引言 工欲善其事必先利其器,而君之“器”尚无,就更别谈“事”了。赶紧!我们需要下载并安装一个 Xilinx Vivado 软件!!接下来就飞速地开始我们的 Xilinx_Vivado_SDK_2019.1 详细安装教程!!&#…

win10安装vivado + vitis 2019.2 教程

win10安装vivado vitis 2019.2 教程 安装包:链接:https://pan.baidu.com/s/1fPlNDzpC0EPXMhOloDyzfA 提取码:1234 网上其他博主的安装教程,比如:vivado2019.2的安装,最后是没有安装上vitis PS端开发软件…

vivado入门教程

vivado入门教程 基本步骤例程实现 第一次写博客,也是第一次使用vivado,自己也在学习之中,欢迎大家的评论啊! 基本步骤 一、新建工程 二、选择工程路径及命名 三、一路next到下图,确定芯片的型号 四、添加源文件 五…

手把手教你安装vivado2015.4开发环境

//vivado2015.4安装教程 //作者:紫菜蛋花汤 //时间:2018.7.8 //版本:V1 准备工作: 1.vivado2015.4安装包 官方下载压缩包文件名:Xilinx_Vivado_SDK_2015.4_1118_2.tar 个人百度云连接:https://pan.ba…

Vivado 2015.4 安装教程(含license)

首先先下载vivado2015.4的压缩文件,可以从网盘里下载: 百度网盘链接:点击链接 下载后解压: 点击xsetup.exe文件 点击Next 点击Next 一定要选择第一个Vivado HL WebPack,不要像图中那样选择第三个,因为第一…

vivado 2017.4安装步骤

目录:windows安装vivado2017.4;虚拟机ubuntu安装vivado 2017.4;ios安装vivado。 一,windows安装vivado2017.4 xilinx官网下载地址为:https://www.xilinx.com/support/download.html 下载完解压后,如图所示…

vivado2019.2安装+license添加教程

vivado2019.2安装license添加教程 注意: 1.电脑的账户名字一定是英文; 2.压缩文件夹有30个G,安装后会更大,需要预留足够的空间。 1.资源链接 百度网盘: https://pan.baidu.com/s/1xEuB-vzoXWpj40fd1wAEFg 密码:tkk1…

[Software]Vivado 2018.2 安装及激活教程

一、安装 1、 解压文件 注:要将压缩文件解压至无中文的路径中。 2、 双击“xsetup.exe”文件,开始安装 3、 不选择新版本,选择继续安装此版本 4、 点击“Next”出现协议,在“I Agree”前都打勾 5、 版本选择,用户可以…

Vivado的下载和安装

本文是自己在安装和使用vivado的一个简单记录。 在安装之前进入官网下载好自己需要版本的安装包,软件安装包比较大,需要花费比较长的一段时间。 vivado官网下载地址 :下载地址 安装好软件后需要使用到license文件,可以去官网申…

Vivado2021.2版本安装教程

Vivado2021.2版本安装教程 2021.2版本提取链接:https://pan.baidu.com/s/12P7twkEVErKmqTmkhTnvMg 提取码:2hyr。 第一步:首先打开解压好的软件,找到xsetup.exe,然后管理员模式运行,出现以下界面,点击Next。…

Vivado安装教程详细版

之前问过大佬,哪个版本的vivado更加稳定,大佬说2017.4,于是俺就安装了一个。 安装步骤: 关闭360等杀毒软件。 下载解压后,双击打开这个xsetup程序: 弹出 点击Continue 三个I Agree全部勾选,…

Vivado 2018.3 安装步骤及 license 获取

本文的主要内容是介绍 Vivado 2018.3 版本的安装步骤及其 license 的获取与加载。 首先下载安装包,将其在没有中文的路径下解压。注意在解压前最好关闭电脑的杀毒软件,防止某些文件被拦截或者删除! 解压完成后打开文件夹,在最底部…

Vivado的安装以及使用_入门

Vivado的安装以及使用 零. Vivado简要介绍 Vivado是FPGA厂商赛灵思提供的一款EDA(Electronic Design Automation)工具. 在电子设计自动化方面, 其主要提供了四种功能: RTL代码编写, 功能仿真, 综合(synthesis)以及实现(implementation). 其中, RTL代码…

Vivado安装—Xilinx design tool already exists for 2019.1,specify a different program program group entr

V i v a d o 重 新 安 装 出 现 问 题 ? {\color{Red}Vivado重新安装出现问题?} Vivado重新安装出现问题? V i v a d o 重 新 安 装 出 现 问 题 ? {\color{Red}Vivado重新安装出现问题?} Vivado重新安装出现问题&…

vivado2017.4安装教程

安装前先关闭杀毒软件和360卫士,注意安装路径不能有中文,存放安装包的路径最好也不要有中文。 1、解压安装包到当前文件夹 2、运行安装程序。 3、提示下载最新的版本,不要下载,点击Continue,然后点击next。 4、点击I Agree&#x…