多线程死锁的预防和避免&开发中的注意事项

死锁的定义

在一组进程发生死锁的情况下,这组死锁进程中的每一个进程,都在等待另一个死锁进程所占有的资源。或者说每个进程所等待的事件是该组中其他进程释放所占有的资源。

举个例子:如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。这时线程A获取了锁a,线程B获取了锁b,线程A想要继续获取锁b,但是锁b被线程B占有;线程B想要继续获取锁a,但是锁a被线程A占有,这样线程A和线程B都在等待对方释放资源,也就是形成了资源获取的闭环,导致死锁的发生。

 

产生死锁的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

  1. 互斥条件:进程要求对所分配的资源进行排它性使用,即在一段时间内,某 资源仅为一个进程所占有。如果此时若有其他进程请求该资源,则请求进程只能等待,直至占有该资源的进程用毕释放。
  2. 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  3. 不抢占条件:进程已获得的资源在未使用完毕之前,不能被抢占,即只能在进程使用完时自己释放。
  4. 循环等待条件:在发生死锁时,必然存在一个进程—资源的循环等待链,即进程集合{Pl, P2, ..., pn},P0正在等待一个P1占用的资源,其中Pi等待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有。

 

处理死锁的方法

  1. 预防死锁。这是一种较简单和直观的预先预防方法。该方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个来预防产生死锁。预防死锁是一种易实现的方法,已被广泛使用。
  2. 避免死锁。同样是属于事先预防策略,但它并不是事先采取各种限制措施,去破坏产生死锁的四个必要条件,而是在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而可以避免发生死锁。
  3. 检测死锁。这种方法无须事先采取任何限性制措施,允许进程在运行过程中发生死锁。但可通过检测机构及时地检测出死锁的发生,然后采取适当的措施,把进程从死锁中解脱出来。
  4. 解除死锁。当检测到系统中已发生死锁时,就采取相应的措施,把进程从死锁中解脱出来。常用的方法是撤消一些进程,回收它们的资源,将资源分配给已处于阻塞状态的进程,使其能继续运行。

 

代码开发中的注意事项

(1)最理想的状态:两个锁的申请就没有发生交叉,避免了死锁的可能性,这是最理性的情况,因为锁没有发生交叉。如果能够这么理性,就不需要讨论死锁了。

(2)最好是能够避免在一个同步方法中调用其它对象的延时方法和同步方法。一旦我们在一个同步方法中,或者说在一个锁的保护的范围中,调用了其它对象的方法时,就要十分的小心:

  • 如果其它对象的这个方法会消耗比较长的时间,那么就会导致锁被我们持有了很长的时间;
  • 如果其它对象的这个方法是一个同步方法,那么就要注意避免发生死锁的可能性了;

(3)以确定的顺序获得锁,破坏“循环等待条件”。针对多线程需要共同访问的资源进行线性排序,并赋予不同的序号,规定每一个线程必须按照序号递增的顺序请求资源。采用这种策略,如何排序就变得很重要。通常以大多数线程需要的锁或者资源的先后顺序进行排序。

(4)超时放弃,锁持有的时间加一个时限,破坏“请求和保持条件”。当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。

(5)使用事务时,尽量缩短事务的逻辑处理过程,及早提交或回滚事务; (细化处理逻辑,执行一段逻辑后便回滚或者提交,然后再执行其它逻辑,避免开启事务之后,等待用户输入,或者调用服务接口等耗时的操作)

 

死锁检测

Jstack命令

jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。

Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

首先,我们通过jps确定当前执行任务的进程号:

jonny@~$ jps
597
1370 JConsole
1362 AppMain
1421 Jps
1361 Launcher

可以确定任务进程号是1362,然后执行jstack命令查看当前进程堆栈信息:

jonny@~$ jstack -F 1362
Attaching to process ID 1362, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 23.21-b01
Deadlock Detection:


Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock Monitor@0x00007fea1900f6b8 (Object@0x00000007efa684c8, a java/lang/Object),
  which is held by "Thread-0"

"Thread-0":
  waiting to lock Monitor@0x00007fea1900ceb0 (Object@0x00000007efa684d8, a java/lang/Object),
  which is held by "Thread-1"

Found a total of 1 deadlock.

可以看到,进程的确存在死锁,两个线程分别在等待对方持有的Object对象

JConsole工具

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

我们在命令行中敲入jconsole命令,会自动弹出以下对话框,选择进程1362,并点击“链接”

多线程死锁的预防和避免&开发中的注意事项_第1张图片

 

新建连接

进入所检测的进程后,选择“线程”选项卡,并点击“检测死锁”

多线程死锁的预防和避免&开发中的注意事项_第2张图片

 

检测死锁

可以看到以下画面:

多线程死锁的预防和避免&开发中的注意事项_第3张图片

可以看到进程中存在死锁。

 

死锁的解除

死锁检测算法检测出系统中发生了死锁,可以通过:

  1. 抢占资源。从一个或多个进程中抢占足够数量的资源,分配给死锁进程,以解除死锁状态。
  2. 终止(或撤销)进程。终止(或撤销)系统中的一个或多个死锁进程,直至打破循环环路,使系统从死锁状态中解脱出来。

这些是操作系统层面的操作,我们的项目中并不会有死锁检测算法在扫描我们的线程,如果真的不幸发生了死锁,我想重启大法或许是最简单有效的。

 

参考资料:

  • https://blog.csdn.net/jonnyhsu_0913/article/details/79633656
  • https://blog.csdn.net/jonnyhsu_0913/article/details/79633656
  • https://www.cnblogs.com/digdeep/p/4448148.html
  • 《计算机操作系统(第四版)》汤小丹等著 2014年5月第4版

 

你可能感兴趣的:(Java并发编程)