死锁是指,两个或多个动作一直在等待其他动作完成而使得所有动作都始终处在阻塞的状态。想要在开发阶段检测到死锁是非常困难的,而想要解除死锁往往需要重新启动程序。更糟的是,死锁通常发生在负载最重的生产过程中,而想要在测试中发现它,十分不易。之所以这么说,是因为测试线程之间所有可能的交叉是不现实的。尽管出现了一些静态分析库可以帮助我们发现可能出现的死锁,我们还是有必要在运行时检测到死锁,并且得到有用的信息,以便我们解决这个问题或者重启程序,或者做些其他的事情。
在编程中使用ThreadMXBean类来检测死锁
Java 5引入了ThreadMXBean接口,它提供了多种监视线程的方法。我建议您了解所有这些方法,因为当您没使用外部工具时,它们会为您提供很多有用的操作以便您监测程序性能。这里,我们感兴趣的方法是findMonitorDeadlockedThreads,如过您使用的是Java 6,对应的方法是findDeadlockedThreads。二者的区别的是,findDeadlockedThreads还可以检测到owner locks(java.util.concurrent)引起的死锁,而findMonitorDeadlockedThreads只能检测monitor locks(例如,同步块)。由于保留老版本的方法只是出于兼容性的考虑,所以我将使用新版本的方法。在这里,编程的思想是把对死锁的周期性检测封装到一个可重用组件里,之后我们只需启动它、随它去。
一种实现调度的方法是通过执行器框架,即一组良好抽象并易于使用的多线程类。
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
this.scheduler.scheduleAtFixedRate(deadlockCheck, period, period, unit);
就是那么简单,在我们通过选择周期和时间单位而设置了一个特定时间后,就得到了一个周期性调用的线程。接着,我们想使功用得以拓展从而允许用户提供在程序检测到死锁时所触发的行为。最后,我们需要一个方法来接收用于描述死锁中所有线程的一系列对象。
void handleDeadlock(final ThreadInfo[] deadlockedThreads);
现在,实现死锁检测类已经万事俱备了。
public interface DeadlockHandler {
void handleDeadlock(final ThreadInfo[] deadlockedThreads);
}
public class DeadlockDetector {
private final DeadlockHandler deadlockHandler;
private final long period;
private final TimeUnit unit;
private final ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);
final Runnable deadlockCheck = new Runnable() {
@Override
public void run() {
long[] deadlockedThreadIds = DeadlockDetector.this.mbean.findDeadlockedThreads();
if (deadlockedThreadIds != null) {
ThreadInfo[] threadInfos =
DeadlockDetector.this.mbean.getThreadInfo(deadlockedThreadIds);
DeadlockDetector.this.deadlockHandler.handleDeadlock(threadInfos);
}
}
};
public DeadlockDetector(final DeadlockHandler deadlockHandler,
final long period, final TimeUnit unit) {
this.deadlockHandler = deadlockHandler;
this.period = period;
this.unit = unit;
}
public void start() {
this.scheduler.scheduleAtFixedRate(
this.deadlockCheck, this.period, this.period, this.unit);
}
}
让我们动手试试。首先,我们要创建一个handler用来向System.err输出死锁线程的信息。在现实场景中,我们可以用它发送邮件,比如:
public class DeadlockConsoleHandler implements DeadlockHandler {
@Override
public void handleDeadlock(final ThreadInfo[] deadlockedThreads) {
if (deadlockedThreads != null) {
System.err.println("Deadlock detected!");
Map<Thread, StackTraceElement[]> stackTraceMap = Thread.getAllStackTraces();
for (ThreadInfo threadInfo : deadlockedThreads) {
if (threadInfo != null) {
for (Thread thread : Thread.getAllStackTraces().keySet()) {
if (thread.getId() == threadInfo.getThreadId()) {
System.err.println(threadInfo.toString().trim());
for (StackTraceElement ste : thread.getStackTrace()) {
System.err.println("t" + ste.toString().trim());
}
}
}
}
}
}
}
}
这一过程在所有的堆栈追踪中反复进行并为每个线程信息打印对应的堆栈踪迹。通过这种方式,我们可以准确知道每个线程等待的位置和对象。但这个方法有一个缺陷——当一个线程只是暂时等待时,可能会被当作一个暂时的死锁,从而引发错误的警报。出于此,当我们处理死锁时,原始线程不能继续存在而findDeadlockedThreads方法会返回没有此类线程。为了避免可能出现的NullPointerException,我们需要警惕这种情况。最后,让我们促成一个死锁来看看系统是如何运行的。
DeadlockDetector deadlockDetector = new DeadlockDetector(new DeadlockConsoleHandler(), 5, TimeUnit.SECONDS);
deadlockDetector.start();
final Object lock1 = new Object();
final Object lock2 = new Object();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock1) {
System.out.println("Thread1 acquired lock1");
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException ignore) {
}
synchronized (lock2) {
System.out.println("Thread1 acquired lock2");
}
}
}
});
thread1.start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock2) {
System.out.println("Thread2 acquired lock2");
synchronized (lock1) {
System.out.println("Thread2 acquired lock1");
}
}
}
});
thread2.start();
输出:
Thread1 acquired lock1
Thread2 acquired lock2
Deadlock detected!
“Thread-1” Id=11 BLOCKED on java.lang.Object@68ab95e6 owned by “Thread-0” Id=10
deadlock.DeadlockTester$2.run(DeadlockTester.java:42)
java.lang.Thread.run(Thread.java:662)
“Thread-0” Id=10 BLOCKED on java.lang.Object@58fe64b9 owned by “Thread-1” Id=11
deadlock.DeadlockTester$1.run(DeadlockTester.java:28)
java.lang.Thread.run(Thread.java:662)
记住,死锁检测的开销可能会很大,你需要用你的程序来测试一下你是否真的需要死锁检测以及多久检测一次。我建议死锁检测的时间间隔至少为几分钟,因为更加频繁的检测并没有太大的意义,原因是我们并没有一个复原计划,我们能做的只是调试和处理错误或者重启程序并祈祷不会再次发生死锁。如果你有关于解决死锁问题的好建议或者关于这个解决方案的疑问,请在下面留言。