Java 如何快速排查死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象。

(1)实例

死锁的本质,举个例子如果此时有一个线程 A ,按照先获持有锁 a 再获取锁 b的顺序获得锁,同时另外一个线程 B,按照先获取锁 b 再获取锁 a 的顺序获取锁。如下图所示。

image-20200708093303203.png

代码模拟上述死锁过程:

import java.util.concurrent.TimeUnit;

public class DeadLock {

    private static Object lockA = new Object();
    private static Object lockB = new Object();

    public void deadLock() {
        Thread threadA = new Thread(() -> {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "获取 lockA 成功");
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName() + "尝试获取 lockB");
                    synchronized (lockB) {
                        System.out.println(Thread.currentThread().getName() + "获取 lockB 成功");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threadB = new Thread(() -> {
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "获取 lockB 成功");
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName() + "尝试获取 lockA");
                    synchronized (lockA) {
                        System.out.println(Thread.currentThread().getName() + "获取 lockA 成功");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        threadA.start();
        threadB.start();
    }

    public static void main(String[] args) {
        DeadLock deadLock = new DeadLock();
        deadLock.deadLock();
    }
}
deadlock_02.png
(2)通过 jdk 工具 jps、jstack 排查死锁问题

1)使用 jsp 查找程序进行

jps 是 jdk 提供的一个工具,可以查看正在运行的 java 进程。

➜  ~ jps 
57248 Jps
56736 Launcher
56737 DeadLock  # 死锁演示进程
59618

2)使用 jstack 查看线程堆栈信息

jstack 是 jdk 提供的一个工具,可以查看 java 进程中线程堆栈信息。更详细的用法见文档最后。

➜  ~ jstack 56737
deadlock_03.png

从上可以看出死锁的数量以及死锁在代码中出现的位置。

(3)通过 jdk 提供的工具 jconsole 排查死锁问题

jconsole 是 jdk 提供的一个可视化的工具,方便排查程序的一些问题,如:程序内存溢出、死锁问题等等。更详细的用法见文档最后。

jconsole 位于 jdk 的 bin 目录中

➜  bin ./jconsole
deadlock_04.png

上图可以看到我们的程序,点击 connect。

在 jconsole 窗口中查看线程堆栈信息。

deadlock_06.png

点击 DetectDeadlock 可以查看详细的死锁信息,和 jstack 展示的类似。

deadlock_08.png
如何避免死锁

我们知道了死锁如何产生的,那么就知道该如何去预防。如果一个线程每次只能获取一个锁,那么就不会出现由于嵌套持有锁顺序导致的死锁

1)正确的顺序获得锁

上面的例子出现死锁的根本原因就是获取所的顺序是乱序的,超乎我们控制的。上面例子最理想的情况就是把业务逻辑抽离出来,把获取锁的代码放在一个公共的方法里面,让这两个线程获取锁都是从我的公共的方法里面获取。

当Thread1线程进入公共方法时,获取了A锁,另外Thread2又进来了,但是A锁已经被Thread1线程获取了,所以只能阻塞等待。Thread1接着又获取锁B,Thread2线程就不能再获取不到了锁A,更别说再去获取锁B了,这样就有一定的顺序了。只有当线程1释放了所有锁,线程B才能获取。

修改后的例子:

import java.util.concurrent.TimeUnit;

public class DeadLock2 {

    private static Object lockA = new Object();
    private static Object lockB = new Object();

    public void deadLock() {
        Thread threadA = new Thread(() -> {
            getLock();
        });
        Thread threadB = new Thread(() -> {
            getLock();
        });
        threadA.start();
        threadB.start();
    }

    private void getLock() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + "获取 lockA 成功");
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + "尝试获取 lockB");
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "获取 lockB 成功");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        DeadLock2 deadLock2 = new DeadLock2();
        deadLock2.deadLock();
    }
}
deadlock_07.png

2)超时放弃

当线程获取锁超时了则放弃,这样就避免了出现死锁获取的情况。当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。

总结:

死锁就是“两个任务以不合理的顺序互相争夺资源”造成,因此为了规避死锁,应用程序需要妥善处理资源获取的顺序。 另外有些时候,死锁并不会马上在应用程序中体现出来,在通常情况下,都是应用在生产环境运行了一段时间后,才开始慢慢显现出来,在实际测试过程中,由于死锁的隐蔽性,很难在测试过程中及时发现死锁的存在,而且在生产环境中应用出现了死锁,往往都是在应用状况最糟糕的时候——在高负载情况下。因此,开发者在开发过程中要谨慎分析每个系统资源的使用情况,合理规避死锁。

你可能感兴趣的:(Java 如何快速排查死锁?)