死锁是指两个以上线程因争夺资源而发生互相等待的现象!没有外力调解的话,就造成线程一直互相等待,无法执行的情况!
死锁发生会同时出现以下的四个必要条件。
1、互斥
一个资源同时只能被一个线程使用
2、请求并保持
线程在请求资源阻塞的时候,并不会释放其已经拥有的资源。
3、不可剥夺
对于线程已经获得的资源,只能线程自己释放,其他线程无法强制剥夺。
4、循环等待
两个或者两个以上线程出现等待资源出现循环依赖。
死锁共有三种解决方式:
1、预防死锁
预防死锁的解决方法就是对上述的四个必要条件的破坏。
1、互斥条件
这个条件一般是无法打破,因为锁大部分的场景就是同时只能允许一个线程获得。
2、请求并保持
防止线程在持有资源后并申请其他资源的情况。
线程一次性申请所有需要的资源。申请成功就运行,不成功就等待。
缺点:线程会提前申请后期运行需要的资源,会导致其他线程无法获取对应的资源,很多时候是其他线程是不会造成死锁的。会造成线程的饥饿,资源的利用率太低。
3、不可剥夺条件
当想要申请新资源并且失败的时候,将当前拥有的资源释放。当新资源可用时,会重新申请之前放弃的资源。
缺点:反复的放弃和申请资源会造成资源的浪费,降低系统的吞吐量。
4、循环等待条件
将所有的资源从小到大编号,线程申请资源按照编号从小到大申请。
比如说进程P1,使用资源的顺序是R1,R2,进程P2,使用资源的顺序是R2,R1,如果采取动态分配的方式,就很有可能造成死锁。我们对设备进行分类编号,那么P1,P2只能以R1,R2的顺序来申请资源。就可以打破环形回路,避免死锁。
缺点:和之前的请求和保持的缺点一致,可能会提前申请不必要的资源,造成资源浪费,造成其他线程的饥饿,降低资源的利用率。
2、避免死锁
预防死锁是通过打破四个死锁必要条件中的一个。
避免死锁不会这么严格要求必须必须打破某个死锁必要条件,因为即使发生了四个死锁中的四个必要条件中的一个,也不一定会发生死锁。
避免死锁相较于预防死锁条件更加宽松。避免死锁的唯一判断条件就是当前线程申请了这个资源,是否会导致死锁。只允许不会造成死锁的线程申请资源。
怎么判断呢?就是通过大名鼎鼎的银行家算法了
银行家算法牵扯到以下几个数组和矩阵来统计:
1、可利用的资源数组 Available
记录着资源对应的可利用个数,假如有m个资源,对应的就是一个m维的数组。数组中的数据随着资源的分配和回收波动。
2、最大需求矩阵 Max
记录着线程对于每个资源的最大需求资源个数。为一个n*m的矩阵。n为线程的个数,m为资源的个数
3、已分配矩阵 allocated
记录着为线程对于每个资源的已分配资源个数。为一个n*m的矩阵。n为线程的个数,m为资源的个数
4、需求矩阵 need
记录着线程对于每个资源还需要申请的资源个数,为一个n*m的矩阵,n为线程的个数,m为资源的个数。
银行家算法原理:
当一个线程申请的资源大于可利用资源时,就直接申请失败。
如果申请的资源小于等于可利用资源的时候,就进行安全检测。
检查什么呢?
如果将资源分配给线程后,是否存在一条安全执行序列,使各个线程都能安全执行,不发生死锁。如果存在就给其分配资源。如果不存在就申请失败。
缺点: 必须提前确定线程的最大申请资源个数。
3、死锁的检测和解除
死锁的检测和解除允许系统进入死锁状态。
当线程申请资源的时候,会检测死锁。
如果线程请求资源成功或者尝试请求资源的时候,会将线程和资源信息写入一个结构中,比如map中。
当线程A请求资源的时候,会检测当前拥有资源的线程B是否想要请求线程A当前拥有的资源。
事实上线程B可能会请求其他线程占用的资源。比如线程B请求的资源被线程C占用,而线程C请求的资源被线程D占用,就这样一直递归,直到对应的线程没有请求资源或者对应的线程请求的资源被线程A占用。
如果对应的线程没有请求资源,那么就说明A可以请求资源。
如果对应的线程请求了A占用的资源,说明当前系统发生了死锁,开始解除死锁,比如强制剥夺导致死锁的线程A的对应的资源。
缺点: 撤销死锁线程的代价比较大。某些线程都马上执行结束后,却给撤销了,功亏一篑属实是。