使用 jstack 排查死锁
jstack 是 JDK 自带的命令行工具,用于生成 Java 进程的线程快照(thread dump)。通过分析线程快照,可以快速定位死锁问题。
1. 获取 Java 进程 ID
首先,找到目标 Java 进程的 PID(进程 ID)。使用 jps 命令:
jps -l
输出示例:
12345 com.example.MyApplication
67890 sun.tools.jps.Jps
记录下应用进程的 PID,例如 12345。
2. 生成线程快照
使用 jstack 命令生成线程快照:
jstack <pid> > thread_dump.txt
例如:
jstack 12345 > thread_dump.txt
如果应用因死锁导致无响应,可以添加 -l 选项(显示锁信息):
jstack -l 12345 > thread_dump.txt
注意:在某些环境下(如容器),可能需要使用
sudo或以应用运行用户身份执行。
3. 分析线程快照
打开生成的 thread_dump.txt,查找死锁信息。
3.1 查找死锁摘要
jstack 会在线程快照末尾自动检测并输出死锁摘要,类似:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f8b5c00a5a8 (object 0x00000000d5f6e3a0, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f8b5c00b0b0 (object 0x00000000d5f6e3b0, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.example.DeadlockExample.method2(DeadlockExample.java:25)
- waiting to lock <0x00000000d5f6e3a0> (a java.lang.Object)
- locked <0x00000000d5f6e3b0> (a java.lang.Object)
at com.example.DeadlockExample.run(DeadlockExample.java:15)
"Thread-0":
at com.example.DeadlockExample.method1(DeadlockExample.java:18)
- waiting to lock <0x00000000d5f6e3b0> (a java.lang.Object)
- locked <0x00000000d5f6e3a0> (a java.lang.Object)
at com.example.DeadlockExample.run(DeadlockExample.java:10)
3.2 手动分析线程堆栈
如果没有自动摘要(例如使用 jstack 的旧版本),可以手动查找以下特征:
- BLOCKED 状态:线程状态为
BLOCKED(在java.lang.Thread.State中) - 等待锁:堆栈中包含
waiting to lock <0x...>和locked <0x...>
找出相互等待锁的线程对,即线程 A 持有锁 L1 等待锁 L2,线程 B 持有锁 L2 等待锁 L1。
4. 解读死锁信息
从摘要中可以获取:
- 死锁线程名称:
Thread-1和Thread-0 - 锁对象地址:
<0x00000000d5f6e3a0>和<0x00000000d5f6e3b0> - 持有与等待关系:
- Thread-1 持有
<0x...b0>,等待<0x...a0> - Thread-0 持有
<0x...a0>,等待<0x...b0>
- Thread-1 持有
- 代码位置:具体到哪个类的哪一行(如
DeadlockExample.java:25)
5. 解决死锁
根据堆栈定位到代码,常见解决方式:
- 调整锁顺序:确保所有线程以相同的顺序获取锁
- 使用
tryLock:尝试获取锁,超时则释放已持有的锁 - 减少锁粒度:缩小同步块范围
- 使用更高层次的并发工具:如
ReentrantLock、StampedLock、ConcurrentHashMap等
6. 完整示例
以下是一个简单的死锁代码及使用 jstack 排查的演示。
6.1 死锁代码
public class DeadlockExample implements Runnable {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
method1();
} else {
method2();
}
}
private void method1() {
synchronized (lock1) {
System.out.println("Thread-0 持有 lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Thread-0 持有 lock2");
}
}
}
private void method2() {
synchronized (lock2) {
System.out.println("Thread-1 持有 lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Thread-1 持有 lock1");
}
}
}
public static void main(String[] args) {
DeadlockExample example = new DeadlockExample();
Thread t1 = new Thread(example, "Thread-0");
Thread t2 = new Thread(example, "Thread-1");
t1.start();
t2.start();
}
}
6.2 使用 jstack 排查
# 1. 找到 PID
jps -l
# 2. 生成快照
jstack -l 12345 > deadlock.txt
# 3. 查看死锁摘要
grep -A 20 "Found one Java-level deadlock" deadlock.txt
输出类似:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f8b5c00a5a8 (object 0x00000000d5f6e3a0, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f8b5c00b0b0 (object 0x00000000d5f6e3b0, a java.lang.Object),
which is held by "Thread-1"
7. 注意事项
- 如果进程占用 CPU 过高,也可用
jstack查看线程状态,结合top -H -p <pid>找到高 CPU 线程 ID 转十六进制后匹配堆栈。 - 多次生成快照(间隔几秒)有助于对比线程状态变化。
- 在容器中,可能需要进入容器内部执行
jstack,或使用docker exec。 - 若
jstack不可用(如 JVM 未安装完整 JDK),可使用kill -3 <pid>将线程快照输出到标准错误(通常记录在应用日志中)。
通过以上步骤,可以快速定位并解决 Java 应用中的死锁问题。
