如何排查Java多线程死锁问题?这个技巧要掌握

后端 潘老师 3周前 (04-01) 23 ℃ (0) 扫码查看

Java程序一旦出现死锁,参与死锁的线程会陷入无法继续执行的困境,长时间的死锁甚至会导致整个系统卡顿乃至卡死,严重影响服务的正常运行和用户体验。所以,当线上出现死锁问题时,快速准确地排查并解决显得尤为重要。下面,我们就详细介绍一下线上死锁问题的排查思路。

一、死锁问题的识别

当程序出现阻塞、挂起,并且CPU使用率降低等现象时,有可能是发生了死锁。此时,需要进一步确认,通常可以借助一些工具来获取相关信息。

二、线程堆栈信息的收集

在排查死锁问题时,获取线程堆栈信息是关键的一步。我们可以使用jps命令来查看应用程序的进程ID。例如,在终端执行jps命令后,会列出当前运行的Java进程及其对应的ID,找到我们要排查的目标进程ID。

得到进程ID后,利用jstack -l <pid> > ./deadlockdemo.txt命令,将进程ID对应的程序线程日志收集到deadlockdemo.txt文本文件中。这个文件包含了丰富的线程信息,为后续分析死锁原因提供了重要依据。

三、线程堆栈分析与代码定位

收集好线程堆栈信息后,就要对deadlockdemo.txt文件进行分析。在文件中搜索“deadlock”“waiting to lock”“lock”等关键信息,从中找出线程之间的锁竞争关系。

以一个简单的死锁案例为例:

public class DeadLockDemo {
    // 定义两个锁对象
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    // 方法1,先获取lock1锁,休眠100ms后尝试获取lock2锁
    public void method1() {
        synchronized (lock1) {
            System.out.println("Method1 持有lock1");
            try {
                Thread.sleep(100);
                synchronized (lock2) {
                    System.out.println("Method1 持有lock2");
                }
            } catch (InterruptedException e) {
                System.out.println("Method1 等待获取lock2");
            }
        }
    }

    // 方法2,先获取lock2锁,休眠100ms后尝试获取lock1锁
    public void method2() {
        synchronized (lock2) {
            System.out.println("Method2 持有lock2");
            try {
                Thread.sleep(100);
                synchronized (lock1) {
                    System.out.println("Method1 持有lock1");
                }
            } catch (InterruptedException e) {
                System.out.println("Method1 等待获取lock1");
            }
        }
    }

    // 主方法,启动两个线程分别执行method1和method2
    public static void main(String[] args) {
        DeadLockDemo demo = new DeadLockDemo();
        Thread t1 = new Thread(demo::method1, "method1");
        Thread t2 = new Thread(demo::method2, "method2");
        t1.start();
        t2.start();
    }
}

运行上述程序后,会发现程序一直卡住,进程无法结束。通过分析deadlockdemo.txt文件,会发现类似“method2:waiting to lock monitor 0x00007fc0da8548b8 which is held by method1”和“method1:waiting to lock monitor 0x00007fc0da8572a8 which is held by method2”这样的信息,这表明线程method1method2出现了循环等待锁的情况,即发生了死锁,同时也能定位到具体的代码行。

四、代码分析与问题复现

对定位到的代码进行分析,在这个案例中,很容易看出是两个线程获取锁的顺序不当导致了死锁。线程method1先锁定了lock1,然后休眠100ms,再尝试锁定lock2;而线程method2此时已经锁定了lock2,接着又尝试锁定lock1,由于lock1lock2都被对方持有且不释放,导致两个线程的第二次锁定永远无法成功,从而形成死锁。

为了进一步验证问题,我们可以抽离问题代码逻辑,在测试环境中复现死锁问题,确保找到的原因是准确的。

五、代码修正与方案验证

找到死锁原因后,就要对代码进行修正。在上述案例中,调整获取锁的顺序,让线程method2也先锁定lock1,然后再锁定lock2,修正后的代码如下:

public class DeadLockDemo {
    // 定义两个锁对象
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    // 方法1,先获取lock1锁,休眠100ms后尝试获取lock2锁
    public void method1() {
        synchronized (lock1) {
            System.out.println("Method1 持有lock1");
            try {
                Thread.sleep(100);
                synchronized (lock2) {
                    System.out.println("Method1 持有lock2");
                }
            } catch (InterruptedException e) {
                System.out.println("Method1 等待获取lock2");
            }
        }
    }

    // 方法2,调整获取锁的顺序,先获取lock1锁,休眠100ms后尝试获取lock2锁
    public void method2() {
        synchronized (lock1) {
            System.out.println("Method2 持有lock1");
            try {
                Thread.sleep(100);
                synchronized (lock2) {
                    System.out.println("Method2 持有lock2");
                }
            } catch (InterruptedException e) {
                System.out.println("Method2 等待获取lock2");
            }
        }
    }

    // 主方法,启动两个线程分别执行method1和method2
    public static void main(String[] args) {
        DeadLockDemo demo = new DeadLockDemo();
        Thread t1 = new Thread(demo::method1, "method1");
        Thread t2 = new Thread(demo::method2, "method2");
        t1.start();
        t2.start();
    }
}

代码修正后,重新进行测试,确保死锁问题不再出现。在测试过程中,可以模拟多种场景,如高并发访问等,进一步验证方案的有效性。

六、总结排查要点

  1. 识别死锁现象:密切关注应用程序的运行状态,当出现线程长时间阻塞等异常情况时,要警惕死锁的发生。
  2. 获取线程堆栈:熟练使用jpsjstack等工具,准确收集线程堆栈信息,为分析死锁提供数据支持。
  3. 分析定位代码:仔细分析线程堆栈信息,定位到发生死锁的代码区域,深入理解代码逻辑和锁的使用情况。
  4. 优化代码逻辑:根据分析结果,采用合理的方式优化代码,如调整锁的获取顺序、减少锁的粒度、使用非阻塞算法等,避免死锁再次出现。
  5. 监控与测试:在应用上线后,持续监控线程情况,特别是在高并发场景下,通过压力测试和代码审计等手段,尽早发现潜在的死锁问题,确保系统稳定运行。

掌握以上线上Java死锁问题的排查思路和方法,能够帮助我们在遇到死锁问题时迅速响应,有效解决死锁问题。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/16776.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】