美文网首页better
Java 如何快速排查死锁?

Java 如何快速排查死锁?

作者: PC_Repair | 来源:发表于2020-07-08 10:28 被阅读0次

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

    (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 如何快速排查死锁?

        本文链接:https://www.haomeiwen.com/subject/dnuqcktx.html