18.什么情况下java程序会产生死锁?如何定位、修复?
死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。
定位死锁最常用的工具就是利用jstack等工具获取线程栈,然后定位相互之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往jstack工具就能直接定位,类似JConsole甚至可以在图形界面进行有限的死锁检测。
如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决的,只能重启、修正程序本身问题。所以,代码开发阶段相互审查,或者利用工具进行预防性排查,也是很重要的。
写一个基本的死锁程序:
public class DeadLock extends Thread{
private String first;
private String second;
public DeadLock(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";
DeadLock t1=new DeadLock("Thread1", lockA, lockB);
DeadLock t2=new DeadLock("Thread2", lockB, lockA);
t1.start();
t2.start();
t1.join(); //调用join函数的线程执行完毕主线程才会继续运行
t2.join();
}
}
这个程序编译执行后,几乎每次都可以重现死锁。
Thread2 obtained:lockB
Thread1 obtained:lockA
为什么先调用t1.start(),但是t2却先打印出来了。因为线程调度依赖于操作系统调度器,虽然可以通过优先级之类的进行影响,但是具体情况是不确定的。
下面模拟问题定位,jstack。
首先使用jps或者系统的ps命令、任务管理器等工具,确定进程ID:8508。
如上图所示,找到处于BLOCKED状态的线程,按照试图获取(WAITING)的锁ID查找,很快就定位问题。jstack本身也会把类似的简单死锁抽取出来,直接打印出来。
在实际应用中,类死锁情况未必有如此清晰的输出,但是总体上可以理解为:区分线程状态-查看等待目标-对比Monitor等持有状态。
如何在编程中尽量预防死锁?
死锁的发生基本上是因为:
(1)互斥条件,类似java中Monitor都是独占的,要么是我用,要么是你用。
(2)互斥条件是长期持有的,在使用结束之前,自己不会释放,也不能被其他线程抢占。
(3)循环依赖关系。两个或多个个体之间出现了锁的链条环。
据此分析可能的避免死锁的思路和方法:
(1)尽量避免使用多个锁,并且只有需要时才持有锁;
(2)如果必须使用多个锁,尽量设计好锁的获取顺序;
(3)使用带超时的方法,为程序带来更多可控性。
有时候并不是阻塞导致的死锁,只是某个线程进入了死循环,导致其他线程一直等待,这种问题如何诊断?
死锁的另一个好朋友就是饥饿。死锁和饥饿都是线程活跃性问题。实践中死锁可以使用JVM自带的工具进行排查。
死循环死锁可以认为是自旋锁死锁的一种,其他线程因为等待不到具体的信号提示,导致线程一直饥饿。这种情况下可以查看线程CPU使用情况,排查出使用CPU时间片最高的线程,再打出该线程的堆栈信息,排查代码。
基于互斥量的锁如果发生死锁往往CPU使用率较低,实践中也可以从这一方面进行排查。