译文地址:How to Detect Java Deadlocks Programmatically
Introduction:
死锁:两个或多个动作一直在等待其他动作的完成而使得所有动作都始终处于阻塞的状态。
开发阶段检测死锁很困难;测试阶段也很困难-死锁通常发生在负载严重的情况下。解除死锁往往需要重启程序。
尽管有些静态分析库可以帮助我们发现可能出现的死锁,但还是有必须在运行时检测到死锁,得到信息,以便我们解决问题或重启。
Content:
Java 5引入了ThreadMXBean接口,它提供了多种监视线程的方法。我建议您了解所有这些方法,因为当您没使用外部工具时,它们会为您提供很多有用的操作以便您监测程序性能。
重要方法:findDeadlockedThreads(Java 6); findMonitorDeadlockedThreads (Java 5)。(二者的区别的是,findDeadlockedThreads还可以检测到owner locks(java.util.concurrent)引起的死锁,而findMonitorDeadlockedThreads只能检测monitor locks(例如,同步块)。)
编程的思想:把对死锁的周期性检测封装到一个可重用组件里,之后我们只需启动它, 随它去(just fire and forget about it) 。
Example:
实现调度的方法:执行器框架(Executors Framework)-一组良好抽象、易于使用的多线程类。
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
this.scheduler.scheduleAtFixedRate(deadlockCheck, initialDelay, period, timeUnit);
Just so easy : First,通过指定initialDelay(延时)、period(周期)、timeUnit(时间单位),得到一个周期性调用的线程。Second,扩展功能-允许客户端提供在程序检测到死锁后所触发的行为。Finally,创建一个方法-接收描述死锁中所有线程的一系列对象。
void handleDeadlock(final ThreadInfo[] deadlockedThreads);
Interface DeadlockHandler
public interface DeadlockHandler {
void handleDeadlock(final ThreadInfo[] deadlockedThreads);
}
Class DeadlockDetector
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);
}
}
TestCase:
创建一个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();
console 输出:
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)
注:死锁检测的开销可能会很大,你需要用你的程序来测试一下你是否真的需要死锁检测以及多久检测一次。
建议:死锁检测的时间间隔至少为几分钟,因为更加频繁的检测并没有太大的意义,原因是我们并没有一个复原计划,我们能做的只是调试和处理错误或者重启程序并祈祷不会再次发生死锁。