首页 > 编程笔记 > Java笔记 阅读:36

Java排查死锁问题详解(附带实例)

分析和定位死锁问题源头是一项具有挑战性的任务,如果我们遵循一定的步骤和方法,细心分析和查找,是有可能找到死锁问题源头的。

要成功分析和解决死锁,给大家分享笔者这些年在实战中总结出来的方法,按以下步骤即可分析和定位死锁问题源头:
  1. 检测死锁:使用 JConsole 或 VisualVM 来查看线程的堆栈跟踪,或者在代码中添加适当的日志记录,跟踪资源的使用情况;
  2. 分析线程堆栈跟踪:线程堆栈跟踪可以显示每个线程当前的调用堆栈,可以使用 jstack 工具来获取所有线程的堆栈跟踪信息,这对分析线程在何处尝试获取或释放锁是非常有用的;
  3. 寻找循环等待:在堆栈跟踪中寻找循环等待的线程。循环等待通常表现为几个线程相互等待对方释放资源;
  4. 分析和定位死锁:检查代码以确定锁的获取和释放顺序,分析是否有可能导致死锁的情况,发现可能的循环等待之后,回到代码中,审查涉及的线程同步部分;
  5. 解决死锁问题:一旦确定了产生死锁的原因,就可以通过重构代码、改变锁的顺序、使用定时锁、改进资源分配策略等手段来解决死锁问题。

下面我们以一个 Java 并发程序为例,演示如何分析和定位死锁。

假设有两个线程,它们都试图获取两个共享资源的锁,但它们以不同的顺序获取。资源分别是两个对象 lock1 和 lock2。示例代码如下:
public class DeadlockDemo {
    public static void main(String[] args) {
        final Object lock1 = new Object();
        final Object lock2 = new Object();
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: Holding lock 1...");
                try { Thread.sleep(10); } catch (InterruptedException e) {}
                System.out.println("Thread 1: Waiting for lock 2...");
                synchronized (lock2) {
                    System.out.println("Thread 1: Holding lock 1 and 2...");
                }
            }
        });
        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2: Holding lock 2...");
                try { Thread.sleep(10); } catch (InterruptedException e) {}
                System.out.println("Thread 2: Waiting for lock 1...");
                synchronized (lock1) {
                    System.out.println("Thread 2: Holding lock 1 and 2...");
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}
在上述代码中,thread1 首先获取 lock1,然后尝试获取 lock2;而 thread2 首先获取 lock2,然后尝试获取 lock1。如果两个线程同时运行,它们各自持有一个锁并等待另一个锁被释放,这可能导致死锁。

检测死锁

在示例程序运行时,我们会发现两个线程都停止输出,没有一个能够进展到获取两个锁的状态。这是死锁的一个明显迹象。由于该示例比较简单,我们通过运行状态进行判断即可。

针对复杂场景,有时需要使用 JConsole 或 VisualVM 检测工具,或者通过代码中的日志记录来检测与判断。

分析线程堆栈跟踪

我们可以使用 jstack 工具来获取所有线程的堆栈跟踪信息。在命令行中运行“jstack <pid>”命令,其中,<pid> 是 Java 进程 ID。使用 jps 命令查看 JVM 中的 Java 进程信息,其中包含进程 ID。

知晓进程 ID 后,使用 jstack 工具就可以获取所有线程的状态和它们的调用堆栈输出信息了。

寻找循环等待

在 jstack 的输出中,可以看到以下信息:
"Thread-1" #9 prio=5 os_prio=0 tid=0x00007f89dc001000 nid=0x11bb waiting for monitor entry [0x00007f89daffd000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at DeadlockDemo.lambda$main$1(DeadlockDemo.java:26)
        - waiting to lock <0x00000000d62d4b90> (a java.lang.Object)
        - locked <0x00000000d62d4b80> (a java.lang.Object)
"Thread-0" #8 prio=5 os_prio=0 tid=0x00007f89dc000800 nid=0x11ba waiting for monitor entry [0x00007f89db0fe000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at DeadlockDemo.lambda$main$0(DeadlockDemo.java:11)
        - waiting to lock <0x00000000d62d4b80> (a java.lang.Object)
        - locked <0x00000000d62d4b90> (a java.lang.Object)
从上述信息中,我们可以找到循环等待的证据:Thread-1 持有 lock1(地址0x00000000d62d4b80),等待 lock2(地址0x00000000d62d4b90);而 Thread-0 持有 lock2,等待 lock1。

分析和定位死锁

我们返回程序代码中审查这两个线程同步块的实现,就会清楚地知道,死锁发生的原因是两个线程以不同的顺序访问共享资源。想要解决这个问题,确保所有线程以相同的顺序获取锁即可。

解决死锁问题

修改后的代码如下:
// 确保锁总是以相同的顺序获取
Thread thread1 = new Thread(() -> {
    synchronized (lock1) {
        ...
        synchronized (lock2) {
            ...
        }
    }
});
Thread thread2 = new Thread(() -> {
    synchronized (lock1) { // 首先获取lock1对象锁
        ...
        synchronized (lock2) {
            ...
        }
    }
});
通过上述的代码调整,就可以保证当其中一个线程持有 lock1 并等待 lock2 时,另一个线程不会持有 lock2。这样就消除了循环等待条件,从而避免了死锁。

死锁问题的定位和分析通常是迭代的过程,需要大家在实践中多次尝试和调整。

相关文章