本文详细介绍了Java死锁的概念,构建、排查、以及解决办法,帮助程序员了解死锁。
死锁概述
两个或者多个线程互相持有对方所需要的资源(锁),都在等待对方执行完毕才能继续往下执行的时候,就称为发生了死锁。结果就是两个进程都陷入了无限的等待中。一般是有多个锁对象的情况下并且获得锁顺序不一致造成的。
比如两条线程分别使用两个锁A、B,线程一是获得锁顺序是A、B,线程二获得锁顺序是B、A。线程一获得锁A后释放cpu执行权。线程二获得锁B后释放cpu执行权。它们持有对方继续执行所需要的锁,也不能主动释放自己持有的锁,此时发生死锁!死锁的底层是基于锁的特性产生的,基本上锁只能同时被一个线程持有,后续线程将进入等待队列对待!。
死锁的产生的必要条件
死锁产生有四个必要条件,只要系统发生死锁则以上四个条件都必须成立。
互斥条件: 资源是独占的且排他使用,线程互斥使用资源,即任意时刻一个资源只能给一个线程使用,其他线程若申请一个资源,而该资源被另一线程占有时,则申请者等待直到资源被占有者释放。
不可剥夺条件: 线程所获得的资源在未使用完毕之前,不被其他线程强行剥夺,而只能由获得该资源的线程资源释放。
请求和保持条件: 线程每次申请它所需要的资源,在申请新的资源的同时,继续占用已分配到的资源。
循环等待条件: 在发生死锁时必然存在一个线程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个线程等待环路,环路中每一个线程所占有的资源同时被另一个申请,也就是前一个线程占有后一个线程所申请的资源。
事实上循环等待的成立蕴含了前三个条件的成立,似乎没有必要列出然而考虑这些条件对死锁的预防是有利的,因为可以通过破坏四个条件中的任何一个来预防死锁的发生。
构建死锁
下面构建了一个死锁样例,主要是由于获取资源(锁)的顺序不一致导致的。
1 | public class DeadLock { |
运行一段时间后可以发现,控制台不再输出。
死锁检测
遇到死锁问题的时候,我们很容易觉得莫名其妙,而且定位问题也很困难。一般发生死锁时,主要表现为相关线程不再工作,但是并没有抛出异常。
jstack
jstack命令用于生成虚拟机当前时刻的线程快照。
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部资源导致长时间等待等。jstack相比于jconsole更加的原始,没有图形化界面。
jstack 格式: jstack [option] vmid
控制台输入 jps 找到发生死锁的进程
控制台输入 jstack -l 108(108为死锁进程的pid)。出现线程堆栈信息,拉到最下面,即可发现死锁的信息。我们可以看到,这两个线程互相持有对方所需的资源,又互相需要对方的资源,因此发生了死锁。
如何预防和避免死锁
预防:
死锁的预防基本思想打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。
比如:
- 打破互斥条件:允许进程同时访问某些资源
- 打破不剥夺条件:允许进程从占有者占有的资源中强行剥夺一些资源
- 打破请求与保持条件:进程在运行前一次性地向系统申请它所需要的全部资源
- 打破循环等待条件:实行资源有序分配策略
避免:
- 加锁顺序(线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 死锁检测
阿里巴巴中最新的开发规约,里面有对避免死锁的说明,具体如下:
【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。 说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。