死锁只一种特定的程序状态,在实体之间,由于循环依赖导致一直处于等待之中,没有任何个体可以继续前进,死锁不仅仅是线程之间会发生,存在独占的进程之间同样也可能出现死锁,通常来说,我们大多数聚集在多线程场景中的死锁,指的是两个或者多个线程之间,由于相互等待需要对方需要的锁,而永久阻塞的状态。
image
定位死锁最常见的方式就是利用 jstack等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往 jstack等就能直接定位,类似 JConsole甚至可以再图形界面进行有限的死锁监测。
针对死锁,可以深入考察
写个死锁的程序
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 文件分析
image
最后,结合代码分析线程栈信息。上面这个输出非常明显,找到处于BLOCKED状态的线程,按照试图获取( waiting)的锁ID(请看我标记为相同颜色的数字)查找,很快就定位问题。jstack本身也会把类似的简单死锁抽取出来,直接打印出来在实际应用中,类死锁情况未必有如此清晰的输出,但是总体上可以理解为区分线程状态->查看等待目标->对比 Monitor等持有状态。
使用 java 握倛的标准管理API, ThreadMXBean,其直接就提供了 findDeadlockedthreads() 方法
public class DeadLockFinderSample extends Thread {
private String first;
private String second;
public DeadLockFinderSample(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 {
ThreadMXBean mbean = (ThreadMXBean) ManagementFactory.getThreadMXBean();
Runnable dlcheck = new Runnable() {
@Override
public void run() {
long[] threadIds = mbean.findDeadlockedThreads();
if (threadIds != null) {
ThreadInfo[] threadInfos = mbean.getThreadInfo(threadIds);
System.out.println("Detected deadlock threads:");
for (ThreadInfo threadInfo : threadInfos) {
System.out.println(threadInfo.getThreadName());
}
}
}
};
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// 等待1s 每2s 进行一次死锁扫描
scheduler.scheduleAtFixedRate(dlcheck, 1L, 2L, TimeUnit.SECONDS);
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();
}
}
运行结果
Thread1 obtained lockA
Thread2 obtained lockB
Detected deadlock threads:
Thread2
Thread1
Detected deadlock threads:
Thread2
Thread1
Detected deadlock threads:
Thread2
Thread1
死锁的4个条件
避免使用多个锁, 并且只有需要时才持有锁,嵌套的 synchronized 或者 lock 非常容易出现问题。
如果必须要使用锁,尽量设计好锁的获取顺序,可以参考银行家算法。
image
image
image
使用带超时的方法,为程序带来更多可控性。Object.wait(…)或者 CountDownLatch await(…),都支持所谓的 timed wait,我们完全可以就不假定该锁一定会获得,指定超时时间,并为无法得到锁时准备退出逻辑。并发Lock实现,如 ReentrantLock还支持非阻塞式的获取锁操作 tryLock(),这是一个插队行为( barging),并不在乎等待的公平性,如果执行时对象怡好没有被独占,则直接获取锁。有时,我们希望条件允许就尝试插队,不然就按照现有公平性规则等待,一般采用下面的方法
if(lock.tryLock() || lock.tryLock(timeout, unit)){
// ...
}
业界也有一些其他方面的尝试,比如通过静态代码分析(如 FindBugs)去查找固定的模式,进而定位可能的死锁或者竟争情况。实践证明这种方法也有一定作用,请參考担关文档。除了典型应用中的死锁场景,其实还有一些更令人头疼的死锁,比如类加载过程发生的死锁,尤其是在框架大量使用自定义类加载时,因为往往不是在应用本身的代码库中, jstack等工具也不见得能够显示全部锁信息,所以处理起来比较棘手。对此,Java有方文档进行了详细解释。