关于死锁的一些总结

死锁的问题历来是面试中常问的问题, 可是在实际工作中可能几年都遇到不了一次这种问题. 不知是幸运还是不幸, 新进的这家公司刚去就遇到了两个很经典的死锁问题, 这里分享一下排查和解决思路

死锁的发生往往离不开多线程并发,我所遇到的两个场景分别是数据库的并发和代码层面的并发导致的死锁

这里需要先了解数据库的隔离级别和锁的类型

一.数据库的并发

版本: mysql8.0+的版本. 

触发背景: 项目中有通过多线程执行针对同一张表的批量删除和批量插入

死锁现象:  通过日志文件发现DeadlockException异常. 日志上下文出现多线程对同一张表的批量删除和批量插入

在数据库中复现如下:

场景1:

1.mysql数据库默认的隔离级别为可重复读[可重复读的场景下会产生间隙锁[什么是间隙锁可自行百度]]

2.开启A/B两个线程并开启事务

3.事务1执行批量删除[表索引情况: 联合索引:组成字段: A+B+C]

删除SQL: delete from table where a = 555;

当删除的数据是不存在的数据且未触发到唯一索引时, 会从555~∞ 产生间隙锁[GAP],排他锁[X锁]和意向排他锁[IX锁]

4.事务2:执行类似的批量删除:delete from table where a = 566;同样会566~∞产生锁并生成锁并进入锁等待

5.事务1:执行批量插入, 批量插入的记录数为100条, 此时数据库表需生成对应的索引记录, 而此时566~∞ 的锁已被线程B持有,进入锁等待,等待事务2释放锁

6.事务2等待事务1释放间隙锁, 事务1等待事务2 , 形成了两个线程之间的锁等待也就是死锁

7.事务提交时,因多事务间存在锁的相互等待,产生死锁. 抛出deadlock异常

解决思路:

建议将事务隔离级别调为读已提交[read-commited]

场景2: 

数据库隔离级别为读已提交[read-commited] , 但是删除的条件未触发到索引.且事务内执行的是循环删除

1.线程1:  当删除语句未触发到索引时 , 会扫描全表 , 并对符合条件的记录生成X锁,对不符合条件的记录加 IX锁

2.线程2: 当删除语句未触发到索引时 , 会扫描全表 , 并对符合条件的记录生成X锁,对不符合条件的记录加 IX锁 , 但因线程1已持有部分记录的IX锁, 线程2等待线程1释放IX锁

3.线程1: 再次执行批量删除,但是当扫描全表发现部分记录已经被线程2持有X锁 , 这里线程1需要等待线程2释放X锁.

到这里:线程2等待线程1释放IX锁 , 线程1等待线程2释放X锁 , 两者之间形成了相互等待 ,当线程1提交事务时,发现存在相互等待的情况 , 触发死锁

解决思路: 为保持代码逻辑不变的前提下 , 对表增加索引,让删除语句定位到索引上.就不会扫描全表了

可查询表perfermance_scheme.data_locks & data_lock_waits 查看锁及锁等待情况,并持有锁

以上两种情况主要是数据库层面产生的死锁 , 且这种发生死锁的情况在日志中都是可以追溯的,至少代码中会有deadlock的异常信息. 接下来分享一个代码层面的死锁

二.代码层面形成的并发

场景: 因为代码中有段处理的逻辑非常耗时 , 所以有个人把代码暴力拆成了三部分: A/B/C

其中: A/B分别通过异步线程的方式去执行, C部分要等待A&B都执行完毕后才执行.所以C段逻辑放在了主线程中执行.并通过CountDownLatch计时器进行控制等待

到这里其实也没问题, 但是B部分逻辑中有一段逻辑需要持有实例T才可执行 ,而实例T的获取逻辑中又用到了synchronized关键字

到这里,刺激的就来了, 主线程先持有了实例T,,且在等待A&B部分的异步线程执行完才会释放该实例[线程未结束时,实例会一直被主线程持有] , 而异步线程B又在等待得到实例T

好了: 主线程等待异步线程执行完好向下执行 , 异步线程在等待主线程执行完好获得实例T.两者形成了实际意义上的相互等待.也就是死锁

排除思路:

1.看日志: 日志中一切正常  打印的都是正确日志 . 可是执行到一个节点的时候就中断没有向下执行了.

2. 然后去看代码逻辑,发现断点部分在执行synchronized后就进入了等待 , 在看主线程中已经持有了该实例,  发现两个线程间存在相互等待的情况

这个难就难在:日志一切正常.代码逻辑很长.并不会像数据库那种会产生deadlock

触发原因总结

1.实例获取封装使用了synchronized进行单例控制

2.代码中使用了countdownlatch进行线程等待,且未加过期时间 .

导致一个结果: 就是两个线程永远都在相互等待,且因为是占用的内存 , synchronized和countdownlatch也不存在超时中断 , 两个线程永远不会结束 . 实例T永远不会被其他线程获得

上面是通过日志手段排查线程的思路:

还有一种就是: 本地复现的思路 , 可通过jstack命令查看线程等待状态

命令: jstack -l pid[进程ID]> jstack.log生成日志查看当前JVM中线程状态

posted @ 2024-01-21 16:04  每天学习1点点  阅读(26)  评论(0编辑  收藏  举报