典型回答
死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅会发生在线程之间,存在资源独占的进程之间同样也可能出现死锁。通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。
就像下图中所示:
定位死锁最常见的方式就是利用jstack等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往jstack等就能直接定位,类似JConsole甚至可以在图形界面进行有限的死锁检测。
如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决的,只能重启、修正程序本身问题。所以,代码开发阶段互相审查,或者利用工具进行预防性排查,往往也是很重要的。
知识扩展
下面这个程序,只用了两个嵌套的synchronized去获取锁,几乎每次都可以重现死锁:
public class DeadLockSample extends Thread { private String first; private String second; public DeadLockSample(String name, String first, String second) { super(name); this.first = first; this.second = second; } public void run() { synchronized (first) { System.out.println(this.getName() + " obtained: " + first); try { Thread.sleep(1000L); synchronized (second) { System.out.println(this.getName() + " obtained: " + second); } } catch (InterruptedException e) { // Do Nothing } } } public static void main(String[] args) throws InterruptedException { String lockA = "lockA"; String lockB = "lockB"; DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB); DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA); t1.start(); t2.start(); t1.join(); t2.join(); } }
某一次的输出如下:
Thread2 obtained: lockB Thread1 obtained: lockA
这里有个比较有意思的地方,为什么先调用Thread1的start,但是Thread2却先打印出来了呢?这就是因为线程调度依赖于操作系统调度器。虽然你可以通过优先级之类进行影响,但是具体情况是不确定的。
下面来模拟问题定位,这里就选取最常见的jstack。首先,可以使用jps或者系统的ps命令、任务管理器等工具,确定进程ID。第二步,调用jstack获取线程栈:
>jstack your-pid
然后,分析得到的输出,具体片段如下:
Java stack information for the threads listed above: =================================================== "Thread2": at cn.liximing.study.javacore.DeadLockSample.run(DeadLockSample.java:23) - waiting to lock <0x000000078097aea0> (a java.lang.String) - locked <0x000000078097aed8> (a java.lang.String) "Thread1": at cn.liximing.study.javacore.DeadLockSample.run(DeadLockSample.java:23) - waiting to lock <0x000000078097aed8> (a java.lang.String) - locked <0x000000078097aea0> (a java.lang.String) Found 1 deadlock.
上面的输出可以看出,jstack本身也会把类似的简单死锁抽取出来,直接打印出来。但是一些复杂的情况,则需要分析各个线程持有的锁,以及等待的锁进行分析。
如何在编程中尽量预防死锁?
首先,我们来总结一下前面例子中死锁的产生包含了哪些基本元素。基本上死锁的发生是因为:
- 互斥条件,类似Java中Monitor都是独占的,要么是我用,要么是你用。
- 互斥条件是长期持有的,在使用结束之前,自己不会释放,也不能被其它线程抢占。
- 循环依赖关系,两个或者多个个体之间出现了锁的链条环。
据此分析得到以下一些避免死锁的思路和方法。
方法一:如果可能的话,尽量避免使用多个锁,并且只有需要时才持有锁。
程序之所以需要持有多个锁,就是因为需要持有多个资源。从程序设计的角度反思,如果我们赋予一段程序太多的职责,出现“既要……又要……”的情况时,可能就需要我们审视下设计思路或目的是否合理了。
方法二:如果必须使用多个锁,尽量设计好锁的获取顺序。
这个说起来简单,做起来可不容易,你可以参考著名的“银行家算法”。一般的情况,建议可以采取一些简单的辅助手段。比如:将对象(方法)和锁之间的关系,用图形化的方式分别抽取出来。然后根据对象之间组合、调用的关系对比和组合,考虑可能调用的时序。按照可能时序合并,发现可能死锁的场景。
方法三:使用带超时的方法,为程序带来更多可控性。
类似Object.wait(...)或者CountDownLatch.await(...),都支持所谓的timed_wait,我们完全可以不假定该锁一定会获得,指定超时时间,并为无法得到锁时准备退出逻辑。
并发Lock实现,如ReentrantLock还支持非阻塞式的获取锁操作tryLock(),这是一个插队行为(barging),并不在乎等待的公平性。如果执行时对象恰好没有被独占,则直接获取锁。有时,我们希望条件允许就尝试插队,不然就按照现有公平性规则等待,一般采用下面的方法:
if (lock.tryLock() || lock.tryLock(timeout, unit)) { // ... }
方法四:通过静态代码分析工具去查找固定模式。
业界也有一些其它方面的尝试,比如通过静态代码分析(如FindBugs)去查找固定的模式,进而定位可能的死锁或者竞争情况。实践证明这种方法也有一定的作用。
【完】