Ubuntu Java日志中线程死锁的解决方法
jstack是JDK自带的命令行工具,可直接生成Java进程的线程堆栈快照,并自动识别死锁信息。
jps -l
(列出所有Java进程,格式为“PID 主类名”)或ps -ef | grep java
(过滤出Java进程),获取目标应用的PID。例如:$ jps -l
12345 com.example.Application
jstack -l <PID> > thread_dump.txt
(-l
参数表示显示锁的详细信息),将线程堆栈保存到thread_dump.txt
文件中。thread_dump.txt
,搜索“deadlock”关键字,jstack会明确标注死锁线程。例如:Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f8a1200e888 (object 0x000000076b6a4f80, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f8a12011c88 (object 0x000000076b6a4f90, a java.lang.Object),
which is held by "Thread-1"
上述信息表明:Thread-1等待Thread-0持有的锁,Thread-0等待Thread-1持有的锁,形成循环等待。若不熟悉命令行,可使用JDK自带的图形化工具JVisualVM(位于JDK的bin
目录下)。
jvisualvm
(Linux/Mac)或双击jvisualvm.exe
(Windows)。com.example.Application
),双击连接。死锁的产生需满足四个条件:互斥条件(资源独占)、请求与保持条件(持锁又请求新锁)、不可剥夺条件(锁不能被强制释放)、循环等待条件(线程间循环等待锁)。通过破坏其中任意一个条件即可避免死锁:
synchronized (lock1) {
// 处理业务
synchronized (lock2) {
// 处理业务
}
}
// 改为:
synchronized (lock1) {
// 处理业务
}
synchronized (lock2) {
// 处理业务
}
ReentrantLock
的tryLock()
方法,设置超时时间。若无法在规定时间内获取锁,则释放已持有的锁并重试。例如:Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
try {
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
// 处理业务
}
} finally {
lock2.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock1.unlock();
}
lock1
再获取lock2
,避免交叉等待。synchronized
关键字功能有限,易引发死锁。推荐使用java.util.concurrent
包中的高级工具:
synchronized
更灵活。例如:private final Lock lock = new ReentrantLock();
public void doSomething() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
}
private final Semaphore semaphore = new Semaphore(5); // 允许5个线程同时访问
public void doSomething() throws InterruptedException {
semaphore.acquire();
try {
// 临界区代码
} finally {
semaphore.release();
}
}
若死锁已发生且无法通过代码修复,可通过以下方式临时恢复:
jstack
定位死锁线程后,在JVisualVM或jconsole
中终止其中一个线程(如Thread-0或Thread-1),打破循环等待。注意:终止线程可能导致数据不一致,需谨慎使用。