面试题答案
一键面试排查死锁的思路和步骤
-
使用工具获取线程信息:
- jstack命令:在Java应用程序运行时,可以通过
jstack
命令获取Java进程的线程转储信息。例如,假设应用程序的进程ID为1234
,在命令行中执行jstack 1234
,这会输出所有线程的堆栈跟踪信息。在输出中查找处于BLOCKED
状态的线程,因为死锁中的线程通常处于这种状态,并且可能会发现一些线程互相等待锁的迹象。 - Java VisualVM:这是一个可视化的工具,可用于监控和分析Java应用程序。它可以连接到正在运行的Java进程,直观地展示线程的状态、CPU和内存使用情况等。在“线程”标签页中,可以查看每个线程的详细信息,如线程名、状态、堆栈跟踪等,方便定位死锁线程。
- jconsole:也是JDK自带的监控工具,与Java VisualVM类似。连接到目标Java进程后,在“线程”选项卡中查看线程状态,通过分析线程堆栈信息来发现死锁。
- jstack命令:在Java应用程序运行时,可以通过
-
分析线程堆栈信息:
- 当获取到线程堆栈信息后,仔细查看每个线程的调用栈。寻找那些持有锁并且在等待另一个锁的线程。例如,线程A持有锁
Lock1
,并等待锁Lock2
,而线程B持有锁Lock2
,并等待锁Lock1
,这就是典型的死锁场景。 - 关注线程的状态,除了
BLOCKED
状态外,WAITING
状态也可能与死锁相关,特别是在使用wait()
和notify()
机制时。 - 查看线程正在执行的代码位置,结合应用程序的业务逻辑,确定线程在等待什么资源,以及它当前持有的资源是什么。
- 当获取到线程堆栈信息后,仔细查看每个线程的调用栈。寻找那些持有锁并且在等待另一个锁的线程。例如,线程A持有锁
-
结合应用程序逻辑分析:
- 了解应用程序中不同模块之间的交互和资源共享情况。例如,在一个基于Spring框架的Web应用程序中,可能存在多个服务类,这些服务类可能会访问共享的数据库资源或者其他外部资源。
- 如果应用程序使用了数据库连接池,检查是否存在多个线程同时竞争数据库连接的情况。在某些情况下,可能由于事务管理不当,导致线程长时间持有数据库连接锁,进而引发死锁。
- 对于使用分布式缓存(如Redis)的场景,检查是否存在多个线程同时尝试对缓存数据进行读写操作,并且在获取锁的过程中出现循环等待的情况。
避免死锁再次发生的方法
- 调整加锁顺序:
- 确保所有线程按照相同的顺序获取锁。例如,在一个涉及两个资源(如数据库连接和文件系统操作)的场景中,所有线程都先获取数据库连接锁,再获取文件系统操作锁。如果一个线程需要获取多个锁,按照固定的顺序获取可以避免死锁。
- 在代码层面,可以将获取锁的逻辑封装成方法,保证所有使用这些锁的地方都遵循相同的顺序。例如:
public class ResourceLock {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void executeInOrder() {
synchronized (lock1) {
synchronized (lock2) {
// 执行需要同时获取两个锁的业务逻辑
}
}
}
}
- 使用定时锁:
- Java中的
ReentrantLock
类提供了tryLock(long timeout, TimeUnit unit)
方法,可以在指定的时间内尝试获取锁。如果在规定时间内获取不到锁,线程可以选择放弃获取锁,执行其他操作,而不是一直等待。 - 示例代码如下:
- Java中的
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TimedLockExample {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
boolean gotLock1 = false;
boolean gotLock2 = false;
try {
gotLock1 = lock1.tryLock(1, TimeUnit.SECONDS);
if (gotLock1) {
gotLock2 = lock2.tryLock(1, TimeUnit.SECONDS);
if (gotLock2) {
// 执行需要同时获取两个锁的业务逻辑
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (gotLock2) {
lock2.unlock();
}
if (gotLock1) {
lock1.unlock();
}
}
});
Thread thread2 = new Thread(() -> {
boolean gotLock2 = false;
boolean gotLock1 = false;
try {
gotLock2 = lock2.tryLock(1, TimeUnit.SECONDS);
if (gotLock2) {
gotLock1 = lock1.tryLock(1, TimeUnit.SECONDS);
if (gotLock1) {
// 执行需要同时获取两个锁的业务逻辑
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (gotLock1) {
lock1.unlock();
}
if (gotLock2) {
lock2.unlock();
}
}
});
thread1.start();
thread2.start();
}
}
- 死锁检测与恢复:
- 可以在应用程序中定期运行死锁检测逻辑。例如,使用一个定时任务,每隔一段时间获取线程堆栈信息并分析是否存在死锁。
- 如果检测到死锁,可以通过记录日志、发送警报等方式通知开发人员。同时,可以尝试通过终止部分线程来打破死锁,然后重新启动相关的业务流程。不过,这种方式需要谨慎处理,因为终止线程可能会导致数据不一致等问题,需要结合应用程序的具体业务逻辑来实现。
- 优化资源管理:
- 在使用外部资源(如数据库连接、文件句柄等)时,确保资源的及时释放。例如,在使用JDBC连接数据库时,使用
try - finally
块确保连接在使用完毕后被关闭。 - 对于共享资源,可以考虑使用资源池来管理,避免资源的过度竞争。例如,使用连接池来管理数据库连接,线程从连接池中获取连接,使用完毕后归还,这样可以更有效地控制资源的使用。
- 在分布式系统中,使用分布式锁(如基于Redis的分布式锁)时,要确保锁的获取和释放逻辑正确,避免因网络问题等导致锁无法释放,进而引发死锁。可以设置锁的过期时间,防止锁长时间被持有。
- 在使用外部资源(如数据库连接、文件句柄等)时,确保资源的及时释放。例如,在使用JDBC连接数据库时,使用