死锁的概念
- 死锁:由于竞争资源或通信关系,两个或更多进程(线程)在执行过程中出现永远相互等待而引发的事件。死锁对于系统而言是危险的、不安全的,因此在系统的实现时应该考虑避免死锁,或者设计一种可以有效检查死锁并且解决的应急处理方法。
死锁的必要条件
- 互斥
任何时候只能由一个进程使用一个资源实例。 - 持有并等待
进程保持并占有至少一个资源,并且该进程正在等待一个其他进程正在持有的资源。 - 非抢占
资源只能在进程使用后自愿放弃,不存在抢占的情况。 - 循环等待
例如:A0占有A1正在等待的资源,而A1又正占有着A0所请求的资源。这是一个循环等待对方释放资源的循环等待。
死锁的处理
- 死锁预防(Deadlock Prevention)
通过避免出现死锁的四个必要条件来杜绝死锁的出现。一种武断的处理方式,在不影响实际系统性能时可以使用该方式,但是往往会降低系统的效率,所以该方式要慎用! - 死锁避免(Deadlock Avoidance)
在使用进程前来判断,只允许在不会出现死锁的情况下让进程请求资源。当进程数多时,都进行判断检测是一个耗时耗力的过程,在庞大系统中实行起来困难。 - 死锁检测&恢复(Deadlock Detection&Recovery)
在检测到系统进入死锁状态后,进行退回恢复。相较于前两个方式可行性更强,但是需要考虑用什么算法来检测死锁状态,检测的周期是多长,退回恢复操作退回到哪一步等问题。
注意:操作系统对死锁不进行处理,全部交由应用系统来决定。
银行家算法(死锁安全判断)
这里先说明一下银行家算法的数据结构,n是系统现在所有的线程数量,m是可被占有的资源类型数量。Max[ i , j ]是线程 i 对资源 j 的需求量,容量为n×m;Available[ i ]表示资源的剩余空闲量;Allocation[ i , j ]表示线程 i 对资源 j 的占有量,总容量为n×m;Need[ i , j ]表示线程 i 对资源 j 的请求量,总容量为n×m。
在明确了以上数据结构后,下面有一个实例,已知Max矩阵为C,Allocation矩阵为A,那么需求矩阵Need为C-A,剩余资源向量Available根据已知资源总量R减去矩阵A每列总和的向量,得到剩余资源向量V。以上就完成了算法的初始化。
接下来进入算法的核心——算法流程。
下面是死锁检测算法的Java实现:
public class DeadlockDetection {
public int m = 3; //m表示可用资源种类
public int n = 5; //n表示当前存在进程数
public int available[] = new int[m]; //存储每种资源可用数量
public int allocation[][] = new int[n][m]; //存储各个进程对每种资源的占有量
public int request[][] = new int[n][m]; //资源请求表
public boolean finish[] = new boolean[n]; //进程结束表
private int i;
public DeadlockDetection() {
//初始表,可由外部进程创建
available[0] = 0;
available[1] = 0;
available[2] = 0;
allocation[0][0] = 0;
allocation[0][1] = 1;
allocation[0][2] = 0;
allocation[1][0] = 2;
allocation[1][1] = 0;
allocation[1][2] = 0;
allocation[2][0] = 3;
allocation[2][1] = 0;
allocation[2][2] = 3;
allocation[3][0] = 2;
allocation[3][1] = 1;
allocation[3][2] = 1;
allocation[4][0] = 0;
allocation[4][1] = 0;
allocation[4][2] = 2;
request[0][0] = 0;
request[0][1] = 0;
request[0][2] = 0;
request[1][0] = 2;
request[1][1] = 0;
request[1][2] = 2;
request[2][0] = 0;
request[2][1] = 0;
request[2][2] = 1;
request[3][0] = 1;
request[3][1] = 0;
request[3][2] = 0;
request[4][0] = 0;
request[4][1] = 0;
request[4][2] = 2;
}
public void detecte() {
//初始化剩余资源表work[]
int work[] = this.available;
//初始化进程结束表,如果一个进程不占有任何资源,
//说明该进程不处于死锁的必要条件,该进程被认定为结束。
for(int i1 = 0; i1<this.n; i1++){
int x = 0;
for(int i2 = 0; i2<this.m; i2++){
x = x+this.allocation[i1][i2];
}
if(x == 0){
this.finish[i1]=true;
}else {
this.finish[i1]=false;
}
}
//循环n(进程数)次,刷新进程结束表
for(int i =0; i<this.n; i++){
//循环n(进程数)次,遍历各个进程,看剩余资源是否满足某一进程需求
for(int ii1 =0; ii1<this.n; ii1++){
if(this.finish[i]){
work = add(work,this.allocation[i]);
}else{
int flag = 0;
//循环m(资源数)次,遍历该进程的资源需求是否可以被剩余资源满足
for(int ii2=0; ii2<this.m; ii2++){
if(this.request[ii1][ii2]<=work[ii2]) {
flag++;
}
}
if(flag==this.m){
work = add(work,this.allocation[i]);
this.finish[i] = true;
}
}
}
}
//以上可以得出算法复杂度=O(n*n*m)
//循环判定进程结束表,判断是否会产生死锁
for(i= 0;i<this.n;i++){
if(!this.finish[i]){
System.out.println("进程会产生死锁,不安全!!");
return;
}
}
//通过检测,不会产生死锁
System.out.println("进程不存在死锁危险~");
}
//向量加和运算
public int[] add(int[] x,int[] y){
for(int i=0; i<this.m; i++){
x[i] = x[i]+y[i];
}
return x;
}
public static void main(String[] args){
DeadlockDetection system = new DeadlockDetection();
system.detecte();
}
}
以上代码已有充分的备注,这里不再做赘述。
死锁恢复
- 结束进程
- 终止所有死锁进程
- 一次只终止一个导致死锁的进程直到死锁的消除
- 终止进程的优先级应有以下顺序
进程的优先级;
进程已运行事件以及还需运行事件;
进程已占有资源;
进程完成需要的资源;
终止进程数目
- 资源抢占
前面说到非抢占是死锁的一个必要条件,那么这里产生了死锁的进程可以制定抢占算法来打破这一必要条件,从而解除死锁状态。
- 选择被抢占进程
优先选择成本最小的进程。 - 进程回退
返回到一些安全状态,重启进程到安全状态。 - 可能出现饥饿
同一进程可能一直呗选择被抢占者,那么这样这个低优先级低成本的进程就有可能处于饥饿的状态。
小结
死锁状态如上图所示,是一种不安全的状态,所以一个良好的软件应该考虑到如何避免死锁的出现,这样才能确保软件一定的安全性。
(本渣处于学习过程中,有理解不对的地方希望大佬指正!!!)