Java并发之死锁的避免与诊断
一.概述
1.如果一个程序每次至多只能获得 一个锁(这显然不可能出现在实际编程中),那么就不会产生锁顺序死锁。 但是我们可以通过一些策略来避免锁顺序死锁。
2.如果必须获取多个锁,那么在设计时必须考虑锁的顺序:尽量减少潜在的加锁交互数量,将获取锁时需要遵循的协议写入正式文档并始终遵循这些协议。
3.在使用细粒度锁的程序中,可以通过使用一种两阶段策略(Two-Part Strategy)来检查代码中的死锁:首先,找出在什么地方将获取多个锁(使这个集合尽量小),然后对所有这些实例进行全局分析从而确保他们在整个程序中获取锁的顺序都保持一致。要尽可能的使用开放调用,这能极大地简化分析过程。如果所有调用都是开放调用,那么要发现获取多个锁的实例是非常简单的,可以通过代码审查,或者借助自动化的源代码分析工具。
二.具体学习
1.支持定时的锁
有一项技术可以检测死锁和从死锁中恢复过来,就是显式的使用Lock类中的定时tryLock功能来代替内置锁机制。(当使用内置锁时,只要没有获得锁,就会永远等待下去) 。 显式的锁可以指定一个超时的限制(Timeout),在等待超过该时间后tryLock会返回一个失败信息。
1)如果超时的时限比获取锁的时间要长很多,那么就可以在发生某个意外情况后重新获取控制权。
2)当定时锁失败时,你并不需要知道失败的原因。或许是因为发生了死锁,或许某个线程在持有锁时错误地进入了无限循环,还可能是某个操作地执行时间远远超过了我们的预期。 然而我们至少能记录所发生的失败,以及关于这次操作的其他有用的信息,并通过一种更平缓的方式来重新启动计算,而不是关闭整个过程。
3)即使在整个系统中没有始终使用定时锁,使用定时锁来获取多个锁也能有效地应对死锁问题。如果在获取锁时超时,那么可以释放这个锁,然后后退并在一段时间后再次尝试,从而消除了死锁发生的条件,使程序恢复过来。(注意:这项技术只能在同时获取两个锁时才有效,如果在嵌套的方法调用中请求多个锁,那么即使你知道已经持有了外层的锁,也无法释放它)
2.通过线程转储信息来分析死锁
虽然防止死锁的主要责任在于我们自己,但是JVM仍然通过线程转储(Thread Dump)来帮助我们识别死锁的发生。
1)线程转储包括各个运行中的线程的栈追踪信息,这类似于发生异常时的栈追踪信息。线程转储还包含加锁信息,例如每个线程持有了哪些锁,在哪些栈帧中获得这些锁,以及被阻塞的线程正在等待获取哪一个锁。
2)在生成线程转储之前,JVM将在等待关系图中通过搜索循环来找出死锁,如果发现了一个死锁,则获取相应的死锁信息,例如在死锁中涉及哪些锁和线程,以及这个锁的获取操作位于程序的哪些位置。
3)在UNIX平台上触发线程转储操作可以通过向JVM的进程发送SIGQUIT信号或者在UNIX平台中按下ctrl-\键,在Windows平台中按Ctrl-Break键。 在许多IDE中都可以请求线程转储。
下面我们来演示下在IDEA下如何查询线程转储:
首先我们写下如下代码:
import java.util.concurrent.ExecutorService;