目录
一、什么是死锁?
死锁是指多个线程在竞争资源时,互相持有对方需要的资源,且都不愿释放,最终导致所有线程永久阻塞的僵局。
举个生活中的例子:甲拿着钥匙 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
用于打印线程栈信息,直接定位死锁。操作步骤
- 查看进程 ID:打开命令行,输入
jps
(Windows/Linux/Mac 通用),找到DeadlockDemo
对应的进程 ID- 打印线程栈:输入
jstack 1234
,查看输出结果。如果存在死锁,jstack
会在最后明确标注Found 1 deadlock.
,并列出死锁线程的详细信息
【2】VisualVM(图形化工具,直观)
VisualVM 是 JDK 自带的图形化监控工具,能可视化展示线程状态,一键检测死锁。
操作步骤
- 启动 VisualVM:在 JDK 的
bin
目录下找到jvisualvm.exe
(Windows)或jvisualvm
(Linux/Mac),双击启动;- 查看进程:在左侧 “应用程序” 列表中,找到
DeadlockDemo
进程,双击进入;- 检测死锁:切换到 “线程” 标签页,点击右上角的 “检测死锁” 按钮,工具会自动识别死锁线程,并高亮显示(红色),同时给出详细的锁持有信息。
【3】Arthas(阿里开源,线上排查神器)
Arthas 是阿里开源的 Java 诊断工具,支持线上环境排查,无需重启服务。
操作步骤
- 启动 Arthas:下载 Arthas 压缩包,执行
java -jar arthas-boot.jar
,选择DeadlockDemo
对应的进程 ID;- 排查死锁:输入命令
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控制资源访问数量,避免多线程争抢同一资源。
六、总结
理解本质:死锁是 4 个必要条件同时满足的结果,破坏任意一个即可避免;
会模拟:通过 “持有不同锁 + 互相等待” 的场景,快速复现死锁;
会排查:熟练使用 jstack/VisualVM/Arthas,定位死锁线程;
会预防:优先通过 “统一资源顺序”“一次性获取资源” 等方案,从编码阶段规避死锁。
感谢你花时间读到这里~ 如果你觉得这篇内容对你有帮助,不妨点个赞让更多人看到;如果有任何想法、疑问,或者想分享你的相关经历,欢迎在评论区留言交流,你的每一条互动对我来说都很珍贵~ 我们下次再见啦!😊😊