Java 多线程 - 死锁问题
锁是非常有用的工具,运用场景非常多,因为它使用起来非常方便,而且易于理解。但同时它也会带来一些困扰,那就是可能引起死锁。
1. 什么是死锁
百度百科中对于死锁的定义:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
简而言之,当线程1持有资源A,线程2持有资源B。此时线程1想要获取资源B,线程2想要获取资源A。两个线程都想要获取对方手中的资源,自己又不肯让出已有资源,一直僵持不下就形成了死锁。
2. 死锁产生的四个条件
- 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源。
3. 案例
public class DeadLock {
private OtherService otherService;
public void setOtherService(OtherService otherService) {
this.otherService = otherService;
}
// DeadLock的实例的锁-资源A
private final Object LOCK = new Object();
public void m1() {
synchronized (LOCK) {
System.out.println("********m1********");
otherService.s1();
}
}
public void m2() {
synchronized (LOCK) {
System.out.println("********m2********");
}
}
}
public class OtherService {
private DeadLock deadLock;
public void setDeadLock(DeadLock deadLock) {
this.deadLock = deadLock;
}
// OtherService的实例的锁-资源B
private final Object LOCK = new Object();
public void s1() {
synchronized (LOCK) {
System.out.println("========s1========");
}
}
public void s2() {
synchronized (LOCK) {
System.out.println("========s2========");
deadLock.m2();
}
}
}
public class DeadLockTest {
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
OtherService otherService = new OtherService();
deadLock.setOtherService(otherService);
otherService.setDeadLock(deadLock);
new Thread(() -> {
while (true) {
deadLock.m1();
}
}, "T1").start();
new Thread(() -> {
while (true) {
otherService.s2();
}
}, "T2").start();
}
}
上面的案例中,两个线程 T1 和 T2 , 其中 T1 线程调用 DeadLock 的 m1 方法,在 m1 方法内部又调用了 OtherService 的 s1 方法,s1 和 m1 这两个方法都含有用 synchronized 关键字修饰的同步代码块。 T2 线程调用 OtherService 的 s2 方法,在 s2 方法内又调用的 DeadLock 的 m2 方法,同样的,s2 和 m1 这两个方法都含有用 synchronized 关键字修饰的同步代码块。整个程序如图所示:
当 T1 线程执行的时候,m1 方法获取 DeadLock 的 LOCK 锁,并调用 OtherService 的 s1 方法,同时,T2 线程也开始执行,T2 线程获取到 OtherService 的 LOCK 锁,并调用 DeadLock 的 m2 方法,但是由于 m2 的方法的锁此时已经被 T1 线程占有,T2 线程只能等待 T1 线程释放锁,同理,T1 线程也在等待 T2 线程释放锁,于是就形成了死锁。
我们使用 jstack 来观察一下死锁。
首先看到这两个线程互相持有对象的锁,在等待对方释放锁。
jstack 的信息最后也会告诉我们找到一个死锁。
4. 如何避免死锁
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用 lock.tryLock(timeout) 来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。