什么是死锁?怎么预防?如何解决?
在并发编程中,死锁(Deadlock) 是一个常见的问题,尤其是在涉及多线程时。死锁指的是两个或多个线程互相等待对方释放资源,从而进入无限等待的状态,导致整个系统无法继续执行。理解死锁的成因及预防机制,对于编写高效、安全的多线程程序至关重要。
一. 什么是死锁?
死锁是指两个或多个线程互相等待对方持有的资源而永远无法继续执行的状态。在 Java 中,死锁通常发生在使用 synchronized
关键字或者其他锁机制进行同步操作时。如下图,两个线程在保持自己持有的资源的同时永久等待另一个线程持有的资源,就会导致死锁现象的发生。
二. 死锁的必要条件
死锁的产生通常依赖于以下四个条件,这四个条件同时满足时,就有可能发生死锁:
- 互斥条件:线程对所需的资源具有独占访问权,也就是说某个资源在同一时刻只能被一个线程占用。
- 请求和保持:线程已经持有了一个资源,并且还在等待获取另外一个资源。
- 不可剥夺:资源不能被强制从一个线程中剥夺,线程必须自愿释放它占用的资源。
- 循环等待:存在一个线程循环等待链,链中的每个线程都等待下一个线程持有的资源。
只要四个条件同时存在,就有可能导致死锁。
三. Java 中死锁的代码示例
让我们通过一个简单的demo展示 Java 中可能发生死锁的情况。
public class DeadlockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Thread 1: Holding lock 1 & 2...");
}
}
}
public void method2() {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 2 & 1...");
}
}
}
public static void main(String[] args) {
DeadlockExample deadlock = new DeadlockExample();
Thread thread1 = new Thread(deadlock::method1);
Thread thread2 = new Thread(deadlock::method2);
thread1.start();
thread2.start();
}
}
可以看出,thread1
先获取 lock1
,然后试图获取 lock2
;thread2
先获取 lock2
,然后试图获取 lock1
。由于 thread1
和 thread2
分别在等待对方释放资源,从而发生了死锁。
四、java中如何检测死锁
在 JVM 中,通过使用 jstack
命令可以查看线程堆栈信息,从而发现死锁的情况。
执行以下命令可以获取 Java 进程的线程堆栈:
jstack <pid>
jstack
输出中会明确指出是否有死锁存在,并列出哪些线程处于死锁状态。
五、如何预防死锁
上面第二节说了死锁的四个必要条件,只有同时满足这四个条件时才会发生死锁,那么我们在预防死锁时只需要破坏其中的一个条件即可,下面我们对四个必要条件的破坏进行逐个分析:
-
破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
-
破坏请求与保持条件 :一次性申请所有的资源。
-
破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
-
破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
以上是预防死锁的理论基础,那么实际生产环境中我们怎么预防死锁呢?
1. 避免嵌套锁
嵌套锁是导致死锁的主要原因之一。尽量避免一个线程同时持有多个锁,可以通过重构代码来减少对多个资源的锁定。例如,将大部分逻辑放在持有单个锁的代码块中。
2. 固定锁的顺序
如果线程必须持有多个锁,确保所有线程以相同的顺序请求锁。通过这种方式,可以避免循环等待情况的发生。例如,如果所有线程总是先请求 lock1
,然后再请求 lock2
,就可以防止死锁。
3. 使用 tryLock()
Java 的 ReentrantLock
提供了 tryLock()
方法,它尝试获取锁,如果获取失败,它不会阻塞线程,而是返回 false
。这样可以避免线程陷入死锁。
4. 锁超时机制
如果使用 ReentrantLock
,可以在 tryLock()
中设置超时参数。如果线程在指定时间内未能获得锁,便自动放弃锁请求。这有助于避免长时间的锁等待而导致的死锁。
5. 避免过多锁
尽量减少对锁的依赖,使用无锁算法、原子变量(如 AtomicInteger
)或者并发集合(如 ConcurrentHashMap
),这些数据结构能够在高并发的情况下提供线程安全而无需显式的锁机制。
六、如何解决死锁
在检测到死锁之后,怎么解决已经发生的死锁呢?
修改代码!
大多数死锁的产生都是因为代码有问题,我们需要找出哪些线程和哪些资源导致了死锁,进而找到问题代码。下面介绍了一些优化代码的方法:
1、固定锁的获取顺序
2、使用 tryLock()
代替 synchronized
3、
使用超时策略
4、精细锁的粒度
实际上还是终止程序来预防死锁。