在 Java 应用中,死锁(Deadlock) 是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象 ,导致这些线程都无法继续执行下去,程序"卡死"。这是一种非常典型的并发问题。
一、什么是 Java 死锁?
死锁产生的四个必要条件(经典条件,缺一不可):
- 互斥条件:资源一次只能由一个线程占用。
- 占有并等待:线程持有至少一个资源,并等待获取其他被占用的资源。
- 非抢占条件:线程已获得的资源在未使用完之前不能被其他线程强行夺取,必须自己释放。
- 循环等待条件:存在一个线程的循环等待链,每个线程都在等待下一个线程所占用的资源。
二、如何查看 / 检测 Java 中的死锁?
Java 提供了多种方式来 检测死锁 ,下面介绍几种 最常用、最实用的方法:
方法一:使用 jstack 工具查看死锁(推荐,简单有效)
步骤:
-
找到 Java 应用的进程 ID(PID)
在 Linux / macOS 终端 或 Windows 的命令行下运行:
bashjps输出类似:
12345 MyApp 6789 Jps其中
12345就是你的 Java 应用的进程ID(PID)。你也可以使用
ps -ef | grep java(Linux/macOS)或任务管理器(Windows)查找。 -
使用 jstack 查看线程堆栈,并检测死锁
执行:
bashjstack <PID>例如:
bashjstack 12345 -
在输出内容中搜索关键字:
deadlock或Found one Java-level deadlock如果存在死锁,jstack 会自动检测并在输出中明确标识出来,例如:
Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00007f... (object 0x00000000d5d8a3d0, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00007f... (object 0x00000000d5d8a3e0, a java.lang.Object), which is held by "Thread-1"它会清楚地告诉你哪些线程在互相等待哪些锁,从而形成死锁环。
🔍 小技巧:
-
如果输出内容太多,可以将结果重定向到文件查看:
bashjstack 12345 > thread_dump.txt然后打开
thread_dump.txt搜索deadlock或dead-lock。
方法二:使用 jconsole 或 VisualVM 图形化工具查看死锁
这些是 Java 自带的 GUI 工具,可以直观地查看线程状态和死锁。
1. jconsole(JDK 自带)
-
启动方式:
bashjconsole -
在弹出的界面中,选择你的 Java 进程;
-
切换到 "线程" 标签页;
-
点击 "检测死锁"(Detect Deadlock) 按钮;
-
如果存在死锁,它会列出死锁涉及的线程和锁信息。
2. VisualVM(JDK 自带,功能更强大)
-
启动方式:
bashjvisualvm -
选择你的 Java 进程;
-
切换到 Threads(线程) 标签;
-
如果存在死锁,会有明确的提示,也可以看到线程互相等待的情况;
-
也可以使用插件增强功能(如 Visual GC 等)。
🧠 这两个工具适合开发和测试环境,不需要额外安装,JDK 中自带有。
方法三:在代码中 编程检测死锁(高级用法,不常用)
Java 提供了一个 ThreadMXBean 接口,可以 以编程方式检测死锁。
示例代码:
java
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
public class DeadlockDetector {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
System.err.println("检测到死锁!涉及以下线程:");
for (long threadId : deadlockedThreads) {
java.lang.management.ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId);
System.err.println(threadInfo.getThreadName() + " 正在等待锁,被 " +
threadInfo.getLockOwnerName() + " 持有");
}
} else {
System.out.println("未检测到死锁。");
}
}
}
你可以定期调用这个检测逻辑(比如通过定时任务),或在监控系统中集成,用于自动化检测死锁。
三、如何避免和解决死锁?
检测到死锁不是终点,关键还是要 避免死锁发生 ,以下是常见的 死锁预防 / 解决策略:
✅ 1. 避免嵌套锁 / 按固定顺序获取锁
死锁常常发生在多个线程以不同顺序获取多个锁时。
解决方案:
- 所有线程按照相同的顺序获取锁,例如总是先获取锁A,再获取锁B,不要有的线程先A后B,有的先B后A。
🔒 反面例子(容易死锁):
java
// 线程1:锁A -> 锁B
// 线程2:锁B -> 锁A
synchronized(lockA) {
synchronized(lockB) { ... }
}
✅ 正确做法:统一顺序,如都先获取 lockA,再 lockB
✅ 2. 使用锁超时机制
使用如 ReentrantLock.tryLock(long timeout, TimeUnit unit) 方法,尝试获取锁,如果在指定时间内获取不到,就放弃,避免一直等待。
示例:
java
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 执行业务
} finally {
lock.unlock();
}
} else {
// 获取锁失败,做相应处理,避免死等
}
✅ 3. 减少同步代码块的范围
- 只对必要的代码加锁,尽快释放锁;
- 避免在持有多个锁的情况下执行耗时操作(如 IO、网络请求等)。
✅ 4. 使用更高级的并发工具替代 synchronized
- 如使用 java.util.concurrent 包中的工具类 :
ReentrantLock、Semaphore、CountDownLatch、ConcurrentHashMap等; - 这些工具更灵活,有些支持超时、中断等机制,能更好地避免死锁。
四、总结:如何查看 Java 死锁(快速版)
| 方法 | 工具/命令 | 说明 | 是否需要重启/侵入代码 |
|---|---|---|---|
| jstack | 命令行工具 jstack <pid> |
查看线程 dump,自动检测死锁,搜索 deadlock 关键字 |
❌ 不需要,最常用 |
| jconsole | JDK 自带 GUI 工具 | 图形化界面,点击"检测死锁"按钮 | ❌ 不需要 |
| VisualVM | JDK 自带 GUI 工具 | 更强大的线程和内存监控,支持死锁检测 | ❌ 不需要 |
| 编程检测 | ThreadMXBean.findDeadlockedThreads() |
可编程检测死锁,适合嵌入监控系统 | ✅ 需写代码 |
| 日志与监控 | 结合 APM 工具(如 SkyWalking、Arthas) | 生产环境推荐结合工具持续监控 | ✅ 可选 |
✅ 推荐做法(最佳实践):
-
开发阶段:
- 避免随意嵌套 synchronized 或多个锁;
- 使用 jstack 定期检查线程状态,特别是在高并发测试时;
- 使用工具如 jconsole / VisualVM 实时观察线程情况。
-
生产环境:
- 通过 jstack 定时抓取线程 dump(比如通过脚本或监控工具);
- 结合 APM 工具(如 Arthas、SkyWalking、Prometheus + Grafana)实时监控死锁和线程阻塞;
- 发现死锁后,通过日志定位代码,优化锁策略。