死锁发生的四个必要条件
- 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
- 占有和等待:已经得到了某个资源的进程可以再请求新的资源。
- 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
- 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。
处理方法
主要有以下四种方法:
-
鸵鸟策略
把头埋在沙子里,假装根本没发生问题。
因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。
当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。
大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。
-
死锁检测与死锁恢复
不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。
1. 每种类型一个资源的死锁检测
(将进程和资源视作节点,画出资源分配有向图,基于任意节点的DFS)
依次以每个节点为根节点进行深度优先搜索。
如何结束,即如何判断有环(死锁)?
当链表中新加入的节点在之前已经加入过,则说明有环,有死锁。
维护一个链表,初始化为空表。以任意节点为起点出发,将当前节点加入链表表头,沿着当前节点检测是否存在从该节点出发的没有被标记的有向边(一旦有向边被访问过,就会被标记),有的话就沿着这条边找到下一个节点,并作为当前节点;如果没有找到,再若此节点为根节点,则没有环,算法结束,若此节点不是根节点,说明进入了死胡同,因此回退到之前的节点,并将之前的节点作为新的起点;如此进行。
以上图为例:(从左到右DFS)
R到A,A到S,进入死胡同;S倒退到A,A没有没有访问过的有向边,A倒退到R;
R到B,B到T,T到E,E到V,V到G,G到U,U到D,D到S,死胡同,S回退到D,D到T,T重复出现,有环有死锁,结束。
2. 每种类型多个资源的死锁检测
基于资源矩阵的死锁检测算法
进程起初都是没有标记的,算法开始会对进程进行标记,进程标记后就表明他们能够被执行,不会进入死锁。当算法结束的时候,没有标记的进程都是死锁进程。
E是现有资源的总和,A是分配给各个进程之后还剩下的可用资源;
C是各个进程所占有的资源,R是各个进程所要请求的资源;每一行代表一个进程,每一列代表一个资源类型
算法:
1)寻找一个没有标记的进程Pi,它的R矩阵中的第i行向量小于A(说明Pi请求的资源可以被满足,Pi可以完美运行);
2)如果能找到Pi,那么将C矩阵的第i行向量加到A向量;(Pi成功运行完,释放所占有的资源,A资源向量就会回炉);
3)如果没有这样的进程,那么算法终止;、
以上面的图为例子:
三个进程、四种资源;按照C矩阵三行,从上到下依次称为进程1、进程2、进程3;
进程1显然不能运行,因为Blu-rays;
进程2显然也不能运行,因为没有扫描仪;
进程3可以完美运行,运行完成之后,释放资源,此时A为[2,2,2,0],此时进程2又可以运行了,运行完成进程2释放资源,此时A为[4,2,2,1],进程1又可以完美运行了;因此三个进程都被标记,没有死锁进程。
问题:何时进行检测?或者说检测的周期怎么确定?
1)每当有资源请求的时候去检测,但是会占用CPU时间。
2)每隔K分钟检测一次。或者设定一个阈值,当CPU的利用率降到了这个阈值就进行死锁检测(因为,如所有死锁进程,且数量增多,那么在一定时间内就没有多少线程在运行了,此时就会造成CPU空闲,利用率下降)
3. 死锁恢复
- 利用抢占恢复
将一个进程所持有的资源临时分配给另一个进程,并将当前被剥夺资源的进程挂起,直到被分配资源的进程执行完成,归还资源,被挂起进程继续执行;
选择挂起那个进程(也就是剥夺那个进程的资源),取决于那个进程所占有的资源比较好回收
- 利用回滚恢复
类似于数据库里的回滚操作。周期性地将进程的当前状态写入一个文件,新的检查点不应该覆盖旧的检查点。一旦出现死锁,可按照文件中的检查点(回滚点),将进程回滚到某个时间点。
- 通过杀死进程恢复
最简单也是最直接的方法就是,杀死一个或者若干个环内进程直到破坏死锁环。或者杀死一个环外的进程,令其释放资源,让死锁解开。但是选择杀死那个进程需要仔细斟酌。
死锁预防(破坏死锁的四个必要条件)
在程序运行之前预防发生死锁。
1. 破坏互斥条件
例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
2. 破坏占有和等待条件
一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。(缺点:进程开始之前,并不能完全确定所有需要的资源)
3. 破坏不可抢占条件
4. 破坏环路等待
给资源统一编号,进程只能按编号顺序来请求资源。
死锁避免
在程序运行时避免发生死锁。
如果进程在请求资源的时候会导致系统进入不安全状态,则拒绝分配资源;如果不会,则分配。
1. 安全状态和不安全状态
图 a 的第二列 Has 表示已拥有的资源数,第三列 Max 表示总共需要的资源数,Free 表示还有可以使用的资源数。从图 a 开始出发,先让 B 拥有所需的所有资源(图 b),运行结束后释放 B,此时 Free 变为 5(图 c);接着以同样的方式运行 C 和 A,使得所有进程都能成功运行,因此可以称图 a 所示的状态时安全的。
定义:如果没有死锁发生,并且即使所有进程突然请求对资源的最大需求,也仍然存在某种调度次序能够使得每一个进程运行完毕,则称该状态是安全的。反之为不安全状态。
安全状态的检测与死锁的检测类似,因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类似,可以结合着做参考对比。
单个资源的银行家算法(迪杰斯特拉提出的)
一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。
上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。
而 图 b为安全状态,可以CBAD。
多个资源的银行家算法
上图中有五个进程,四个资源。左边的图表示已经分配的资源,右边的图表示还需要分配的资源。最右边的 E、P 以及 A 分别表示:总资源、已分配资源以及可用资源,注意这三个为向量,而不是具体数值,例如 A=(1020),表示 4 个资源分别还剩下 1/0/2/0。
检查一个状态是否安全的算法如下:
- 查找右边的矩阵是否存在一行小于等于向量 A。如果不存在这样的行,那么系统将会发生死锁,状态是不安全的。
- 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。
- 重复以上两步,直到所有进程都标记为终止,则状态时安全的。
可以任意次序分配资源,但是不一定任意次序都是安全的,也就是不一定所有的任意次序的请求都会被允许
如果一个状态不是安全的,需要拒绝进入这个状态。
活锁和饥饿
饥饿
线程饥饿是另一种活跃性问题,也可以使程序无法执行下去。如果一个线程因为处理器时间全部被其他线程抢走而得不到处理器运行时间,这种状态被称之为饥饿,一般是由高优先级线程吞噬所有的低优先级线程的处理器时间引起的。
java语言在Thread类中提供了修改线程优先级的成员方法setPriority,并且定义了10个优先级级别。不同操作系统有不同的线程优先级,java会把这10个级别映射到具体的操作系统线程优先级上边。操作系统的线程调度会按照自己的调度策略来轮番执行我们定义的线程。
我们所设置的线程优先级对操作系统来说只是一种建议,当我们尝试提高一个线程的优先级的时候,可能起不到任何作用,也可能使这个线程过度优先执行,导致别的线程得不到处理器分配的时间片,从而导致饿死。所以我们尽量不要修改线程的优先级,具体效果取决于具体的操作系统,并且可能导致某些线程饿死。
举个栗子:
我们还可以把处理器想象成皇帝,把各个线程想象成妃子,皇帝隔几分钟就换一个妃子陪他。我们设置线程优先级就像是调整某个妃子的好看程度,具体皇帝挑不挑这个妃子还是具体的皇帝说了算,而且不同的皇帝有不同的口味,最后结果是啥还真说不准。如果我们把一个妃子弄的很好看,一个皇帝太宠信她,从而使某些妃子得不到宠信,就是传说中的`饥饿`现象。
活锁
虽然不会像死锁那样因为获取不到资源而阻塞,也不会像饥饿那样得不到处理器时间而无可奈何,活锁仍旧可以让程序无法执行下去~
比如在一间教室里,狗哥要出去,猫爷要进来,门只能容得下一个人进出,而它们在门口相遇了,所以狗哥往后退了一步意思是猫爷先进,而猫爷也退了一步意思是狗哥先进;之后狗哥往前走了一步,猫爷也往前走了一步,俩人又都堵在了门口,所以又都同时退一步,然后再同时进一步,同时退一步,同时进一步…..
把狗哥和猫爷都比做一个线程的话,这两个线程虽然都没有停止运行,但是却无法向下执行,这种情况就是所谓的活锁。
为了解决这个问题,需要在遇到冲突重试时引入一定的随机性。比如狗哥和猫爷在门口相遇都后退时,狗哥隔一秒后再前进,猫爷隔两秒后再前进,这样就不会有同时走到门口的尴尬了~
总结:
死锁主要的点在于:
1)死锁是什么?
如果一个进程集合中的每一个进程都在等待只能由该集合进程中的其它的进程才能引发的事件,那么该进程集合就是死锁的。
2)死锁的四个必要条件
3)死锁发生之后如何检测?如何恢复?
DFS、基于资源矩阵 ||||| 抢占恢复、回滚恢复、杀死进程、
4)死锁在程序运行之前如何预防?
破坏四个必要条件
5)死锁在程序运行时如何避免?
单个资源的银行家算法 多个资源的银行家算法
6)活锁和饥饿?