一、什么是死锁
死锁指两个进程/线程互相占有对方需要的资源,两个进程/线程都申请对方占有的资源,同时自己还把持住资源不释放,就会导致两个线程始终处于互相等待的状态,就造成了死锁。
二、死锁的必要条件
更严谨一点说,在一个系统中以下四个条件同时成立,那么就能引起死锁:
- 互斥条件:至少有一个资源处于非共享模式,也就是某个资源一次只能被一个进程使用,如果另一进程申请该资源,那么申请进程应等到该资源被释放之后才能使用。
- 不可剥夺条件:进程所占有的资源在未使用完毕之前,不能被其他进程强行夺走,只能由占有该资源的进程主动释放
- 请求与保持条件:进程已经占有了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已占有的资源同时被链中下一个进程所请求。
以上这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。所以解决死锁的思路也就是破坏死锁的四个必要条件中的一个或几个。
三、死锁的解决方式
如果死锁已经发生,只能进行死锁的解除,解除死锁的关键在于释放一部分死锁进程占有的资源,让其他进程能够顺利执行,常见的做法有:
1、资源剥夺法:强行挂起或撤销某些进程,释放这些进程占有的资源
2、进程回退法:让某些进程回退到能够避免死锁的地步,释放被占有的资源,这种方法也增加了实现的难度和复杂度,要求保留执行的历史信息并设置还原点。
相比发生死锁之后解除死锁,更重要的还是死锁的预防和避免,常见的避免死锁的方式有:
1、限制加锁的顺序,也就是说不同进程按照相同的顺序加锁
2、设置锁的超时时间,给每个锁加一个超时时间,超过这个时间必须释放锁(例如:基于数据库的分布式锁)
3、死锁检测,在既无法限制加锁顺序也不能限制锁的占有时间情况下,还有一种避免死锁的方式,就是进行死锁检测,系统对进程发出的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配,这是一种保证系统不进入死锁状态的动态策略。(常用于关系性数据库的死锁避免方式)
四、死锁示例
4.1 java示例
线程1和线程2都占有对方的锁且不释放,同时又都申请对方占有的锁,导致死锁
public class DeadLock {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
quietSleep(500);
synchronized (lock2) {
System.out.println("thread1 running");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
quietSleep(500);
synchronized (lock1) {
System.out.println("thread2 running");
}
}
});
thread1.start();
thread2.start();
}
private static void quietSleep(int interval) {
try {
Thread.sleep(interval);
} catch (InterruptedException e) {
// ignore
}
}
}
4.2 mysql示例
有一张表test,只有两个字段:id和name,id是主键,name上没有索引
建表语句:
create table test(id int primary key, name varchar(20));
test表中初始有两条数据:
id | name |
---|---|
1 | a |
5 | c |
打开两个mysql的session,两个session都处于默认的可重复读隔离级别,分别按照顺序执行以下语句:
序号 | session1 | session2 |
---|---|---|
1 | begin | |
2 | begin | |
3 | insert into test values(4,‘b’) | |
4 | update test set name=‘d’ where name=‘c’ | |
5 | insert into test values(3,‘e’) |
执行到第五步,会发现session2已经出现死锁的错误提示了:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
原因解释:
session1中在执行第三步的插入语句的时候会对id=4的这一行申请行锁(x锁),在事务未结束之前锁不会释放,在session2中执行第四步的时候,由于where条件中选择的是没有索引的name字段,所以会逐行加锁:id=1的行锁,2-3的间隙锁(gap锁),到了id=4这一行也会尝试加行锁,这个时候发现session1已经在这一行加了行锁,所以session2就会阻塞住;这个时候session1执行第五步,在id=3这一行进行插入,会尝试先在这一行加行锁,结果发现session2已经在这一行加了间隙锁,于是死锁出现了。当然mysql不会真的让死锁出现,mysql会自动撤销线程2的事务,并给出报错信息。