什么情况下Java程序会产生死锁?如何定位、修复?
死锁
死锁只一种特定的程序状态,在实体之间,由于循环依赖导致一直处于等待之中,没有任何个体可以继续前进,死锁不仅仅是线程之间会发生,存在独占的进程之间同样也可能出现死锁,通常来说,我们大多数聚集在多线程场景中的死锁,指的是两个或者多个线程之间,由于相互等待需要对方需要的锁,而永久阻塞的状态。
产生死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
定位死锁
定位死锁最常见的方式就是利用 jstack等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往 jstack等就能直接定位,类似 JConsole甚至可以再图形界面进行有限的死锁监测。
针对死锁,可以深入考察
- 抛开字面上的概念,让面试者写一个可能死锁的程序,顺便也考察下基本的线程编程。
- 诊断死锁有哪些工具,如果是分布式环境,可能更关心能否用API实现吗?
- 后期诊断死锁还是挺痛苦的,经常加,如何在编程中尽量避免一些典型场景的死锁,有其他工貝辅助吗?
写个死锁的程序
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; } /** * @see java.lang.Thread#run() */ @Override 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) { e.printStackTrace(); } } } /** * @param args * @throws InterruptedException */ 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
jstack 线程 dump 文件分析
定位死锁
定位死锁最常见的方式就是利用 jstack等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往 jstack等就能直接定位,类似 JConsole甚至可以再图形界面进行有限的死锁监测。
针对死锁,可以深入考察
- 抛开字面上的概念,让面试者写一个可能死锁的程序,顺便也考察下基本的线程编程。
- 诊断死锁有哪些工具,如果是分布式环境,可能更关心能否用API实现吗?
- 后期诊断死锁还是挺痛苦的,经常加,如何在编程中尽量避免一些典型场景的死锁,有其他工貝辅助吗?
写个死锁的程序
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; } /** * @see java.lang.Thread#run() */ @Override 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) { e.printStackTrace(); } } } /** * @param args * @throws InterruptedException */ 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
jstack 线程 dump 文件分析
- 然后根据对象之间组合、调用的关系对比和组合,考虑可能调用时序。
按照可能时序合并,发现可能死锁的场景。
方法三
使用带超时的方法,为程序带来更多可控性。Object.wait(…)或者 CountDownLatch await(…),都支持所谓的 timed wait,我们完全可以就不假定该锁一定会获得,指定超时时间,并为无法得到锁时准备退出逻辑。并发Lock实现,如 ReentrantLock还支持非阻塞式的获取锁操作 tryLock(),这是一个插队行为( barging),并不在乎等待的公平性,如果执行时对象怡好没有被独占,则直接获取锁。有时,我们希望条件允许就尝试插队,不然就按照现有公平性规则等待,一般采用下面的方法
if(lock.tryLock() || lock.tryLock(timeout, unit)){ // ... }
方法四
业界也有一些其他方面的尝试,比如通过静态代码分析(如 FindBugs)去查找固定的模式,进而定位可能的死锁或者竟争情况。实践证明这种方法也有一定作用,请參考担关文档。除了典型应用中的死锁场景,其实还有一些更令人头疼的死锁,比如类加载过程发生的死锁,尤其是在框架大量使用自定义类加载时,因为往往不是在应用本身的代码库中, jstack等工具也不见得能够显示全部锁信息,所以处理起来比较棘手。对此,Java有方文档进行了详细解释。
- https://docs.oracle.com/javase/7/docs/technotes/guides/lang/cl-mt.html
- https://plugins.jetbrains.com/plugin/3847-findbugs-idea