[核心技术36问]18.什么情况下java程序会产生死锁?如何定位、修复?

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。

[核心技术36问]18.什么情况下java程序会产生死锁?如何定位、修复?_第1张图片

[核心技术36问]18.什么情况下java程序会产生死锁?如何定位、修复?_第2张图片

    如上图所示,找到处于BLOCKED状态的线程,按照试图获取(WAITING)的锁ID查找,很快就定位问题。jstack本身也会把类似的简单死锁抽取出来,直接打印出来。

    在实际应用中,类死锁情况未必有如此清晰的输出,但是总体上可以理解为:区分线程状态-查看等待目标-对比Monitor等持有状态。

 

如何在编程中尽量预防死锁?

    死锁的发生基本上是因为:

(1)互斥条件,类似java中Monitor都是独占的,要么是我用,要么是你用。

(2)互斥条件是长期持有的,在使用结束之前,自己不会释放,也不能被其他线程抢占。

(3)循环依赖关系。两个或多个个体之间出现了锁的链条环。

据此分析可能的避免死锁的思路和方法:

(1)尽量避免使用多个锁,并且只有需要时才持有锁;

(2)如果必须使用多个锁,尽量设计好锁的获取顺序;

(3)使用带超时的方法,为程序带来更多可控性。

 

有时候并不是阻塞导致的死锁,只是某个线程进入了死循环,导致其他线程一直等待,这种问题如何诊断?

    死锁的另一个好朋友就是饥饿。死锁和饥饿都是线程活跃性问题。实践中死锁可以使用JVM自带的工具进行排查。

    死循环死锁可以认为是自旋锁死锁的一种,其他线程因为等待不到具体的信号提示,导致线程一直饥饿。这种情况下可以查看线程CPU使用情况,排查出使用CPU时间片最高的线程,再打出该线程的堆栈信息,排查代码。

    基于互斥量的锁如果发生死锁往往CPU使用率较低,实践中也可以从这一方面进行排查。

你可能感兴趣的:(JVM)