什么是死锁?它是如何产生的?
死锁(Dead Lock)指的是两个或两个以上的运算单元(进程、线程或协程),互相持有对方所需的资源,导致它们都无法向前推进,从而导致永久阻塞的问题就是死锁。
比如线程 1 拥有了锁 A 的情况下试图获取锁 B,而线程 2 又在拥有了锁 B 的情况下试图获取锁 A,这样双方就进入相互阻塞等待的情况,如下图所示:
死锁的代码实现如下:
public class DeadlockDemo {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1 acquired lock1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 1 acquired lock2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2 acquired lock2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread 2 acquired lock1");
}
}
});
thread1.start();
thread2.start();
}
}
在上面的示例中,我们创建了两个锁 lock1 和 lock2,并在两个线程中分别获取这两个锁,但是获取的顺序不同。当 thread1 获取 lock1 后,它会在持有锁 lock1 的情况下尝试获取 lock2,而当 thread2 获取 lock2 后,它会在持有锁 lock2 的情况下尝试获取 lock1。如果这两个线程启动后,thread1 先获取 lock1 并且在获取 lock2 之前休眠,那么 thread2 就会获取 lock2,然后在尝试获取 lock1 时被阻塞。此时,thread1 就会在获取 lock2 时被阻塞。两个线程都在等待对方释放锁,从而形成了死锁。
死锁 4 大条件
死锁的产生需要满足以下 4 个条件:
- 互斥条件:指运算单元(进程、线程或协程)对所分配到的资源具有排它性,也就是说在一段时间内某个锁资源只能被一个运算单元所占用。
- 请求和保持条件:指运算单元已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它运算单元占有,此时请求运算单元阻塞,但又对自己已获得的其它资源保持不放。
- 不可剥夺条件:指运算单元已获得的资源,在未使用完之前,不能被剥夺。
- 环路等待条件:指在发生死锁时,必然存在运算单元和资源的环形链,即运算单元正在等待另一个运算单元占用的资源,而对方又在等待自己占用的资源,从而造成环路等待的情况。
只有以上 4 个条件同时满足,才会造成死锁。
解决死锁
死锁的常用解决方案有以下两个:
-
按照顺序加锁:尝试让所有线程按照同一顺序获取锁,从而避免死锁。
-
设置获取锁的超时时间:尝试获取锁的线程在规定时间内没有获取到锁,就放弃获取锁,避免因为长时间等待锁而引起的死锁。
死锁排查工具
有一些工具可以帮助排查死锁问题,常见的工具有以下几个:
-
jstack:可以查看 Java 应用程序的线程状态和调用堆栈,可用于发现死锁线程的状态。
-
jconsole 和 JVisualVM:这些是 Java 自带的监视工具,可以用于监视线程、内存、CPU 使用率等信息,从而帮助排查死锁问题。
-
Thread Dump Analyzer(TDA):是一个开源的线程转储分析器,可用于分析和诊断 Java 应用程序中的死锁问题。
-
Eclipse TPTP:是一个开源的性能测试工具平台,其中包含了一个名为 Thread Profiler 的工具,可以用于跟踪线程运行时的信息,从而诊断死锁问题。
本文已收录至《Java面试突击》,专注 Java 面试 100 年,查看更多:www.javacn.site