搞定 Java 并发死锁:原理拆解 + 工具实操 + 预防落地

目录

一、什么是死锁?

二、死锁的 4 个必要条件:缺一不可

三、实战:手写一个死锁案例

四、死锁怎么排查?3 种实用工具

【1】jps + jstack(最常用,命令行工具)

【2】VisualVM(图形化工具,直观)

【3】Arthas(阿里开源,线上排查神器)

五、如何预防死锁?

【1】 破坏 “循环等待”:统一资源获取顺序

【2】破坏 “持有并等待”:一次性获取所有资源

【3】破坏 “不可剥夺”:超时自动释放资源

【4】避免 “互斥”:使用共享资源(按需)

【5】使用并发工具类:替代手动加锁

六、总结


一、什么是死锁?

        死锁是指多个线程在竞争资源时,互相持有对方需要的资源,且都不愿释放,最终导致所有线程永久阻塞的僵局

        举个生活中的例子:甲拿着钥匙 A,等着乙的钥匙 B 开门;乙拿着钥匙 B,等着甲的钥匙 A 开门 —— 两人都不放手,永远卡在原地,这就是死锁。


二、死锁的 4 个必要条件:缺一不可

死锁的产生并非偶然,必须同时满足以下 4 个条件,只要破坏其中任意一个,就能避免死锁。这是理解死锁的核心,也是预防死锁的理论基础。

条件名称核心含义举例(Java 场景)
互斥条件资源只能被一个线程持有,无法共享一个synchronized锁只能被一个线程获取
持有并等待线程持有已获取的资源,同时等待其他资源线程 1 持有锁 A,还想获取锁 B;线程 2 持有锁 B,还想获取锁 A
不可剥夺条件线程已持有的资源,无法被其他线程强制剥夺线程 1 获取锁 A 后,除非自己释放,否则线程 2 无法抢走锁 A
循环等待条件多个线程形成 “资源需求环”,互相等待对方的资源线程 1→锁 B→线程 2→锁 A→线程 1,形成闭环

三、实战:手写一个死锁案例

光说不练假把式,我们用 Java 代码模拟一个典型的死锁场景,直观感受死锁的发生过程。

场景设计

  • 定义两个锁对象 lockA和 lockB;
  • 线程 1 先获取 lockA,休眠 1 秒(模拟业务操作),再尝试获取 lockB;
  • 线程 2 先获取 lockB,休眠 1 秒(确保线程 1 已拿到 lockA),再尝试获取 lockA;
  • 最终两个线程互相等待对方的锁,陷入死锁。
public class DeadlockDemo {
    // 定义两个锁对象(资源)
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        // 线程1:持有lockA,等待lockB
        new Thread(() -> {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + " 已获取 lockA,等待 lockB...");
                try {
                    // 休眠1秒,确保线程2先拿到lockB
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 尝试获取lockB,此时线程2已持有lockB,陷入等待
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + " 已获取 lockB");
                }
            }
        }, "Thread-1").start();

        // 线程2:持有lockB,等待lockA
        new Thread(() -> {
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + " 已获取 lockB,等待 lockA...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 尝试获取lockA,此时线程1已持有lockA,陷入等待
                synchronized (lockA) {
                    System.out.println(Thread.currentThread().getName() + " 已获取 lockA");
                }
            }
        }, "Thread-2").start();
    }
}

运行结果

程序会卡在以下输出,永远不会结束 —— 死锁已发生:

Thread-1 已获取 lockA,等待 lockB...
Thread-2 已获取 lockB,等待 lockA...

四、死锁怎么排查?3 种实用工具

当程序卡死时,如何确认是否是死锁?以下 3 种工具是 Java 开发排查死锁的 “利器”,简单易用。

【1】jps + jstack(最常用,命令行工具)

jps 用于查看 Java 进程 ID,jstack 用于打印线程栈信息,直接定位死锁。

操作步骤
  1. 查看进程 ID:打开命令行,输入 jps(Windows/Linux/Mac 通用),找到 DeadlockDemo 对应的进程 ID
  2. 打印线程栈:输入 jstack 1234,查看输出结果。如果存在死锁,jstack 会在最后明确标注 Found 1 deadlock.,并列出死锁线程的详细信息

【2】VisualVM(图形化工具,直观)

VisualVM 是 JDK 自带的图形化监控工具,能可视化展示线程状态,一键检测死锁。

操作步骤
  1. 启动 VisualVM:在 JDK 的 bin 目录下找到 jvisualvm.exe(Windows)或 jvisualvm(Linux/Mac),双击启动;
  2. 查看进程:在左侧 “应用程序” 列表中,找到 DeadlockDemo 进程,双击进入;
  3. 检测死锁:切换到 “线程” 标签页,点击右上角的 “检测死锁” 按钮,工具会自动识别死锁线程,并高亮显示(红色),同时给出详细的锁持有信息。

【3】Arthas(阿里开源,线上排查神器)

Arthas 是阿里开源的 Java 诊断工具,支持线上环境排查,无需重启服务。

操作步骤
  1. 启动 Arthas:下载 Arthas 压缩包,执行 java -jar arthas-boot.jar,选择 DeadlockDemo 对应的进程 ID;
  2. 排查死锁:输入命令 thread -b-b 表示查找死锁阻塞的线程),Arthas 会直接输出死锁线程的栈信息和锁持有关系:

五、如何预防死锁?

【1】 破坏 “循环等待”:统一资源获取顺序

让所有线程按照固定的顺序获取资源。例如,规定 “必须先获取 lockA,再获取 lockB”,这样线程 2 不会先拿 lockB,自然不会形成循环等待。

// 线程2修改:先获取lockA,再获取lockB(和线程1顺序一致)
new Thread(() -> {
    synchronized (lockA) { // 先拿lockA
        System.out.println(Thread.currentThread().getName() + " 已获取 lockA,等待 lockB...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (lockB) { // 再拿lockB
            System.out.println(Thread.currentThread().getName() + " 已获取 lockB");
        }
    }
}, "Thread-2").start();

【2】破坏 “持有并等待”:一次性获取所有资源

线程在执行前,先尝试获取所有需要的资源,要么全部拿到,要么全部不拿(拿不到就释放已获取的资源,重新尝试)。例如,用 ReentrantLock 的 tryLock() 方法实现:

private static final ReentrantLock lockA = new ReentrantLock();
private static final ReentrantLock lockB = new ReentrantLock();

// 尝试一次性获取lockA和lockB
private boolean tryAcquireAllLocks() {
    boolean acquiredA = false;
    boolean acquiredB = false;
    try {
        // 尝试获取lockA,500ms超时
        acquiredA = lockA.tryLock(500, TimeUnit.MILLISECONDS);
        if (acquiredA) {
            // 获取lockA成功后,尝试获取lockB
            acquiredB = lockB.tryLock(500, TimeUnit.MILLISECONDS);
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        // 如果没拿到所有锁,释放已拿到的锁
        if (!acquiredA || !acquiredB) {
            if (acquiredA) lockA.unlock();
            if (acquiredB) lockB.unlock();
        }
    }
    return acquiredA && acquiredB;
}

【3】破坏 “不可剥夺”:超时自动释放资源

使用带超时的锁获取方式(如 ReentrantLock.tryLock(timeout)),如果线程在指定时间内没拿到所需资源,就主动释放已持有的资源,避免永久阻塞。

【4】避免 “互斥”:使用共享资源(按需)

如果资源可以共享(无需互斥访问),就尽量使用共享资源。例如,用 ReadWriteLock(读写锁)—— 读操作可以并发执行,只有写操作需要互斥,减少锁竞争。

【5】使用并发工具类:替代手动加锁

Java 提供了很多并发工具类,封装了底层锁逻辑,避免手动加锁导致的死锁。例如:

  • 用 CountDownLatch/CyclicBarrier 实现线程同步,替代synchronized;
  • 用 ConcurrentHashMap 替代 HashMap + synchronized,避免手动锁竞争;
  • 用 Semaphore控制资源访问数量,避免多线程争抢同一资源。

六、总结

  1. 理解本质:死锁是 4 个必要条件同时满足的结果,破坏任意一个即可避免;

  2. 会模拟:通过 “持有不同锁 + 互相等待” 的场景,快速复现死锁;

  3. 会排查:熟练使用 jstack/VisualVM/Arthas,定位死锁线程;

  4. 会预防:优先通过 “统一资源顺序”“一次性获取资源” 等方案,从编码阶段规避死锁。


        感谢你花时间读到这里~ 如果你觉得这篇内容对你有帮助,不妨点个赞让更多人看到;如果有任何想法、疑问,或者想分享你的相关经历,欢迎在评论区留言交流,你的每一条互动对我来说都很珍贵~ 我们下次再见啦!😊😊

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值