【实战】Java多线程死锁排查:从复现到根治全流程
- 问题背景与技术原理(150字)
在Java多线程开发中,死锁是指两个或多个线程因互相持有对方所需的锁,且均不释放自身持有的锁,导致线程永久阻塞的现象。死锁需满足互斥、请求与保持、不可剥夺、循环等待四个必要条件,排查的核心是定位循环等待的锁资源与对应的线程。本文基于JDK 17环境,通过实战案例复现死锁,并讲解3种高效排查与解决方法。
- 死锁场景复现(附代码+架构图)
2.1 复现代码
以下代码模拟两个线程争夺两把锁,因锁获取顺序不一致导致死锁:
public class DeadLockDemo {
// 定义两把锁资源
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
public static void main(String[] args) {
// 线程1:先拿LOCK_A,再拿LOCK_B
new Thread(() -> {
synchronized (LOCK_A) {
System.out.println(Thread.currentThread().getName() + " 持有锁A,等待锁B");
try {
Thread.sleep(1000); // 模拟业务耗时,放大死锁概率
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (LOCK_B) {
System.out.println(Thread.currentThread().getName() + " 同时持有锁A和锁B");
}
}
}, "Thread-1").start();
// 线程2:先拿LOCK_B,再拿LOCK_A
new Thread(() -> {
synchronized (LOCK_B) {
System.out.println(Thread.currentThread().getName() + " 持有锁B,等待锁A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (LOCK_A) {
System.out.println(Thread.currentThread().getName() + " 同时持有锁A和锁B");
}
}
}, "Thread-2").start();
}
}
2.2 运行结果
执行代码后,控制台输出如下内容,程序无后续输出且永不终止,死锁已发生:
Thread-1 持有锁A,等待锁B
Thread-2 持有锁B,等待锁A
- 三种死锁排查方法(从入门到进阶)
方法1:jps + jstack(JDK命令行工具,最常用)
核心原理:jps 获取Java进程ID,jstack 打印线程堆栈信息,定位死锁线程与锁资源。
- 打开命令行,执行 jps 命令,获取 DeadLockDemo 对应的进程ID(如 12345)。
jps
输出示例:12345 DeadLockDemo
- 执行 jstack 进程ID 命令,查看堆栈信息,命令行末尾会直接标记死锁:
jstack 12345
- 关键输出解读:
Found one Java-level deadlock:
=============================
Thread-1:
waiting to lock monitor 0x0000000003033e88 (object 0x000000076ab821c0, a java.lang.Object),
which is held by Thread-2
Thread-2:
waiting to lock monitor 0x0000000003032a48 (object 0x000000076ab821b0, a java.lang.Object),
which is held by Thread-1
输出明确显示Thread-1和Thread-2互相等待对方持有的锁,死锁定位完成。
方法2:JConsole可视化工具(直观易用,适合新手)
核心原理:JConsole是JDK自带的可视化监控工具,可实时查看线程状态,自动检测死锁。
-
命令行执行 jconsole 启动工具,在进程列表中选择 DeadLockDemo 进程,点击连接。
-
切换到线程标签页,点击右下角检测死锁按钮,工具会自动列出死锁线程及锁信息:
◦ 死锁线程:Thread-1、Thread-2
◦ 持有锁与等待锁的对应关系,与jstack输出一致。
方法3:Arthas诊断工具(进阶,适合生产环境)
核心原理:Arthas是阿里开源的Java诊断工具,支持在线排查,无需重启应用。
-
下载并启动Arthas,选择目标进程 DeadLockDemo。
-
执行 thread -b 命令,直接打印死锁线程的详细信息:
thread -b
-
优势:生产环境中可在线排查,支持查看线程的调用栈、CPU使用率等附加信息,排查更全面。
-
死锁解决方案与优化(根治思路)
方案1:统一锁获取顺序(最有效)
打破循环等待条件,所有线程按相同顺序获取锁。修改线程2的锁获取逻辑:
// 线程2修改后:先拿LOCK_A,再拿LOCK_B(与线程1顺序一致)
new Thread(() -> {
synchronized (LOCK_A) {
System.out.println(Thread.currentThread().getName() + " 持有锁A,等待锁B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (LOCK_B) {
System.out.println(Thread.currentThread().getName() + " 同时持有锁A和锁B");
}
}
}, "Thread-2").start();
方案2:使用tryLock限时获取锁(灵活可控)
利用ReentrantLock的tryLock(long timeout, TimeUnit unit)方法,在指定时间内获取不到锁则放弃,打破请求与保持条件:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class DeadLockSolveDemo {
private static final ReentrantLock LOCK_A = new ReentrantLock();
private static final ReentrantLock LOCK_B = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
try {
if (LOCK_A.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 持有锁A,等待锁B");
if (LOCK_B.tryLock(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 同时持有锁A和锁B");
LOCK_B.unlock();
}
LOCK_A.unlock();
} else {
System.out.println(Thread.currentThread().getName() + " 获取锁A超时,放弃");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Thread-1").start();
// 线程2逻辑类似,此处省略
}
}
方案3:避免嵌套锁(从根源减少死锁概率)
简化代码逻辑,尽量减少锁的嵌套使用,降低死锁触发的可能性。
-
常见问题FAQ(3个高频疑问)
-
Q:死锁和活锁的区别是什么?
A:死锁是线程互相阻塞,永不执行;活锁是线程不断尝试获取锁但失败,处于"忙碌等待"状态,程序未阻塞但无进展。
- Q:生产环境中如何预防死锁?
A:① 统一锁获取顺序;② 减少锁嵌套;③ 用tryLock限时获取;④ 定期监控线程状态。
- Q:jstack排查不到死锁怎么办?
A:确认进程ID正确;若为分布式场景,需排查多JVM实例间的资源竞争(如数据库锁、Redis锁)。
- 总结与延伸(互动引导)
本文通过实战案例讲解了Java多线程死锁的复现、3种排查方法及根治方案,核心是打破死锁的四个必要条件。在实际开发中,死锁排查需结合业务场景,生产环境优先使用Arthas或APM工具(如SkyWalking)进行监控。
互动问题:你在开发中遇到过哪些特殊的死锁场景?是如何排查解决的?欢迎在评论区留言讨论~
SEO标签:Java多线程、死锁排查、jstack、Arthas、并发编程