Java中的死锁检测

65
很久以前,我从一本Java参考书上摘录了一句话:"Java没有处理死锁的机制,甚至不会知道发生了死锁。" (《Head First Java》第二版,p.516)

那么,这是什么意思呢?在Java中有没有一种方法来捕捉死锁情况?我的意思是,我们的代码是否有一种方式可以理解死锁情况的发生?

1
Java在这方面与其他语言有所不同吗? - Michael Burr
1
检测死锁并不是不可能的,只是相当困难。 - 1800 INFORMATION
1
大多数数据库将检测到死锁。 - 1800 INFORMATION
Java可能没有可以“处理”死锁的机制,但它确实知道死锁的存在。请阅读我的下面的帖子,我会解释如何…… - Jeach
这个问题不是重复的吗:https://dev59.com/e3NA5IYBdhLWcg3wEpgU - Tiago Cogumbreiro
16个回答

85

JDK 1.5以来,java.lang.management包中提供了非常有用的方法来查找和检查发生的死锁。请参阅ThreadMXBean类的findMonitorDeadlockedThreads()findDeadlockedThreads()方法。

使用这种方法的一种可能方式是拥有一个单独的看门狗线程(或定期任务)来执行此操作。

示例代码:

  ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
  long[] ids = tmx.findDeadlockedThreads();
  if (ids != null) {
     ThreadInfo[] infos = tmx.getThreadInfo(ids, true, true);
     System.out.println("The following threads are deadlocked:");
     for (ThreadInfo ti : infos) {
        System.out.println(ti);
     }
  }

2
Java有这个功能真是太棒了!我觉得我快要崩溃了,差点哭出来。 - tObi

19

JConsole 可以检测运行中应用程序的死锁。


12

JDK 5和6将在完整的线程转储(使用kill -3、jstack、jconsole等获得)中转储持有锁的信息。 JDK 6甚至包含有关ReentrantLock和ReentrantReadWriteLock的信息。通过这些信息,可以诊断死锁并找到锁定循环:线程A持有锁1,线程B持有锁2,要么A请求2,要么B请求1。根据我的经验,这通常是非常明显的。

其他分析工具实际上可以找到潜在的死锁,即使它们没有发生。来自OptimizeIt、JProbe、Coverity等供应商的线程工具是寻找问题的好地方。


11
请注意,使用并发包存在一种非常难以调试的死锁类型。这是指当您有一个可重入读写锁(ReentrantReadWriteLock)时,一个线程获取了读锁,然后尝试进入由另一个正在等待获取写锁的线程持有的监视器(monitor)时,就会出现死锁。使其特别难以调试的是,没有记录谁已经进入了读锁,它仅仅是一个计数器。该线程甚至可能已经抛出异常并且终止,但读计数仍然不为零。
以下是一个示例死锁,早期提到的findDeadlockedThreads方法无法找到:
import java.util.concurrent.locks.*;
import java.lang.management.*;

public class LockTest {

    static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) throws Exception {
        Reader reader = new Reader();
        Writer writer = new Writer();
        sleep(10);
        System.out.println("finding deadlocked threads");
        ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
        long[] ids = tmx.findDeadlockedThreads();
        if (ids != null) {
            ThreadInfo[] infos = tmx.getThreadInfo(ids, true, true);
            System.out.println("the following threads are deadlocked:");
            for (ThreadInfo ti : infos) {
                System.out.println(ti);
            }
        }
        System.out.println("finished finding deadlocked threads");
    }

    static void sleep(int seconds) {
        try {
            Thread.currentThread().sleep(seconds*1000);
        } catch (InterruptedException e) {}
    }

    static class Reader implements Runnable {
        Reader() {
            new Thread(this).start();
        }
        public void run() {
            sleep(2);
            System.out.println("reader thread getting lock");
            lock.readLock().lock();
            System.out.println("reader thread got lock");
            synchronized (lock) {
                System.out.println("reader thread inside monitor!");
                lock.readLock().unlock();
            }
        }
    }

    static class Writer implements Runnable {
        Writer() {
            new Thread(this).start();
        }
        public void run() {
            synchronized (lock) {
                sleep(4);
                System.out.println("writer thread getting lock");
                lock.writeLock().lock();
                System.out.println("writer thread got lock!");
            }
        }
    }
}

真的是一个很好的例子...除了通过jstack查找处于WAITING/BLOCKED状态的线程之外,在这种情况下我们还能寻找其他的指示吗? - Deven Phillips

6

通常情况下,Java不提供死锁检测。 synchronized关键字和内置监视器使得在比其他语言更难以理解死锁。

我建议迁移到使用java.util.concurrent.Lock锁等,以便更容易地理解您的锁定方案。实际上,您可以轻松地实现具有死锁检测的锁接口。算法基本上是遍历锁依赖图并查找循环。


3
自JDK 1.6版本开始,java.util.concurrent.Lock中的锁具有死锁检测功能。请参阅java.lang.management.ThreadMXBean。 - staffan

6
死锁可以通过遵循一个简单的规则来避免:所有线程以相同的顺序声明和释放它们的锁。这样,您永远不会陷入死锁的情况。
甚至餐桌哲学家问题也可以看作是违反此规则的一种情况,因为它使用左右勺子的相对概念,导致不同的线程使用不同的调配勺子的顺序。如果勺子被唯一编号,并且哲学家们都试图首先获得最低编号的勺子,则死锁将不可能发生。
在我看来,预防胜于治疗。
这是我喜欢遵循的两个指南之一,以确保线程正常工作。另一个是确保每个线程都<强>完全负责自己的执行,因为它是任何时候唯一完全知道自己在做什么的线程。
这意味着不要使用Thread.stop调用,使用全局标志(或消息队列或类似的东西)告诉另一个线程您想要采取行动。然后让那个线程执行实际工作。

2
虽然这是标准答案,但并非总是可行的。还有其他避免死锁的方法。例如,您可以使用定时的tryLocks(使用ReentrantLock)来进行回退。 - Alex Miller
如果锁都在同一个函数中,按相同的顺序获取锁只是很容易的事情。但是,如果不同的函数抓取锁并以不同的模式相互调用(这是常见情况),那么保证顺序就变得更加困难了。我经常发现最小化锁定时间并使用单个全局锁的策略可以更好地工作。对于线程责任和通信方面的其他答案表示赞同。 - Warren Dew
1
@Warren,如果你编写代码,你可以控制锁定顺序,无论是从单个函数完成还是其他方式。例如,我过去曾经实现了互斥钥匙环(一组按特定顺序排列的互斥锁),在这种情况下,你可以锁定整个钥匙环,以保证没有死锁。无论使用何种死锁避免策略,最小化锁定时间都是一个好主意,但全局锁的问题在于其范围。通常情况下,它会通过一次锁定太多内容而减慢速度 - 这是一个值得考虑的权衡,但它确实是有成本的。 - paxdiablo
1
@Alex,定时锁定和回退是避免死锁的一种策略,但不幸的是这意味着你并不总是得到你需要的锁(并且可能导致活锁,这通常同样糟糕)。如果您需要获取一组资源的锁,则对各个组件进行有序锁定可以保证既不会死锁也不会活锁,从而实现更公平的分配。 - paxdiablo

5

Java可以检测死锁(虽然不能在运行时检测,但仍然可以诊断和报告它)。例如,当使用稍微修改过的'Saurabh M. Chande'代码(将其改为Java并添加一些时间以保证每次运行都会锁定)时,一旦发生死锁,如果输入以下内容:

kill -3 PID   # where 'PID' is the Linux process ID

它将生成一个堆栈转储,其中包括以下信息:

Found one Java-level deadlock:
=============================
"Thread-0":
     waiting to lock monitor 0x08081670 (object 0x7f61ddb8, a Deadlock$A),
     which is held by "main"
"main":
      waiting to lock monitor 0x080809f0 (object 0x7f61f3b0, a Deadlock$B),
      which is held by "Thread-0"

4
如果您使用的是Java 5,您可以调用java.lang.management.ManagementFactory.getThreadMXBean()获取ThreadMXBean,并在其上调用findMonitorDeadlockedThreads()方法。这将仅查找由对象监视器引起的死锁。在Java 6中,有findDeadlockedThreads()方法,它还会查找由“可拥有同步器”(例如ReentrantLockReentrantReadWriteLock)引起的死锁。

请注意,调用这些方法可能会很昂贵,因此应仅用于故障排除目的。


3

虽然不完全符合您的要求,但当死锁发生时,您可以对进程ID执行“kill -3”,它会将线程转储到标准输出。此外,1.6 JVM还有一些工具以图形界面的方式完成相同的操作。


3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接