死锁

你先放手。
我不,你先放手。
你放我就放……

死锁是程序并发所带来的一个重要问题。死锁问题自诞生以来便不存在完美的解决方案。

内心OS:又是并发,能不能说点儿别的。
答曰:不可以。
在第一台计算机诞生的时候,也许是不需要考虑并发问题的,因为当时计算机的主要功能是计算,用做大量的重复性运算。功能单一嘛,就不用考虑计算的同时,做其他事情了。
但是,我们再看如今的计算机,听着歌,打着游戏,游戏角色挂了还可以抽空浏览个网页……想让计算机同时做这么多事情,不考虑并发能行?再说这还只是PC,服务器端的并发才是最吓人的。
内心OS:真麻烦…….

概述


当某一组进程的每个进程均等待此进程中其他进程所占有得到资源而不释放自己本身所占有的资源,导致进程最终永远都无法得到所申请的资源,这种现象就称为死锁。
资源竞争引起死锁

如图所示的,只是死锁情况的其中一种——资源竞争,除了这一种之外,还包括进程通信引起的死锁以及程序设计不合理引起的死锁。

死锁的条件


事情发生都是有先决条件的,即使是人要发脾气也得有个导火索不是?死锁也是一样,我们称其为Coffman条件:
1. 资源独占:一个资源在同一时刻只能被分配给一个进程。
2. 不可剥夺:资源申请者不能强行地从资源占有者的手上夺取资源。
3. 保持申请:进程在占有部分资源后还可以申请新的资源,而且在申请新的资源时并不释放他已经占有的资源。
4. 循环等待:存在某一进程等待序列,可以使其中的进程出现如图所示的申请队列。
循环等待

如果要发生死锁,则必须同时满足上述所有的条件,换句话说,只要破坏这几种条件的任意一个就可以避免死锁的发生。

死锁的处理


死锁是我们不期望发生的,所以我们想到要去预防死锁的发生,但是我们发现,有时候死锁的情况容易破解,而且预防这种死锁发生又会使得系统资源利用率变低,预防显然不如解锁。
为了预防死锁发生,我们有静态的死锁预防动态的死锁避免两大类方法。

动态的情况不只是避免这一种策略,还包括检测以及恢复策略,只是检测和恢复策略一般用于允许死锁发生的情况。

死锁的预防

死锁预防是保证系统不进入死锁状态的静态策略。

预先分配策略

该策略的基本思想如其名,在进程运行之前一次性的将其所需(申请)的资源分配给他,保证其可以正常的执行完毕,不需要在进程运行时再申请资源。
这可能是解决死锁的最简单的方法了,因为破坏了保持申请这一死锁条件,而且还不需要进行过多的处理。
但是,简单的方法往往弊端也很大,该策略的弊端体现在资源利用率低的方面,因为并不是所有的资源在进程运行时都会被长时间占用,哪怕只是需要一秒,这个资源在进程的生命周期内都是不可以被释放的。

小时候哪有什么积木可以玩,顶多拿那啥祸祸泥巴……
给小孩玩积木的时候一般都是直接把所有的积木块都给了,以免小孩玩着玩着没积木了(对于大多数小孩来说,一箱是用不完的)。

有序分配策略

将所有的资源类完全排序(赋予唯一整数,一般是按优先级由小到大的赋值),命名为等级,套用数学的说法,做一个1-1的函数映射就可以了。
规定:进程必须按照资源编号由小到大的次序申请资源。换句话说,一个进程可以申请某一资源类Ri中的资源实例的充分必要条件是它所占有的资源类Rj的等级小于Ri的等级。

角色扮演类的游戏一般都是有等级制的,而且一般情况下,高等级的是被限制进入新手村的。
即由于占有高等级资源,故而不可以申请低等级的新手村资源的。

通常情况下,按照大多数进程使用资源的次序来给资源类编号,即先使用者排在前,后使用者排在后面。
与预先分配策略相比,有序分配策略是在一定程度上提高了资源利用率的,但也存在着主要缺点,如:合理的安排资源编号

死锁的避免

死锁预防是静态的,而死锁避免则是动态的,即动态检查进程对资源的申请是否会造成死锁。
照例先介绍两项概念:

  • 安全状态:如果系统中所有的进程能够按照某一种次序依次进行,就说系统处于安全状态。
  • 安全进程序列:一个由系统中所有进程组成的一个序列,该序列可以正常执行,不发生死锁,则为安全序列。

上面的解释貌似有些苍白……
不过,这两个概念联合起来即可理解:假设有一个序列< P1,P2,……,Pn >,该序列若是处于安全状态的,那么该序列上的任意一个进程Pi后续执行时所需要的资源数量不超过系统当前所剩于的资源以及所有进程当前所占有资源的和(进程Pi可以等待正在执行的进程结束执行,释放资源)。

请注意:不安全状态不一定是死锁状态,这一点与资源分配图中的环路类似。

银行家算法

这是一个避免死锁的算法。
先定义全局变量:

int n;  //记录当前系统中进程的总数
int m;  //记录资源类的总数(一个资源类可能包含数个实例)
int Available[m];   //用于记录当前各类资源中的资源实例,初始为系统资源总量
int Claim[n][m];    //记录每个进程所需各类资源实例的最大量
            //若Claim[i][j] = k,表示进程Pi需资源类Rj的实例个数为K个
int Allocation[n][m];   //记录每个进程当前所占有各类资源实例的数量
            //若Allocation[i][j] = k,表示进程Pi占有资源类Rj的实例个数为K个
int Need[n][m]; //记录每个进程尚需各个资源类的的资源实例数量
        //Need[i][j] = k,表示进程Pi尚需资源类Rj的资源实例个数为K个
int Request[n][m];  //记录每个进程当前所申请的各类资源的资源实例数量
            //Request[i][j] = k,表示进程Pi申请Rj中K个资源实例

银行家算法——资源分配算法

你以为这样就结束了?没看到B4上还有一次检测吗?

int Work[m];    //m定义不变,记录可用资源
int Finish[n];  //n定义不变,记录进程是否可以执行完毕

银行家算法——安全性检测算法

听完理论,看看实践起来是什么样的吧。

死锁的发现


如果系统不使用死锁预防策略,也不使用死锁避免策略,那么系统就会发生死锁,如此我们需要系统检测到死锁,并且消除他。
这就是另一种死锁的处理策略——允许死锁发生,但在死锁发生时,检测并消除其所带来的影响。
也称解锁

死锁检测算法

我们说了,死锁一旦发生,那么我们必须消除他,否则其所带来的危害对于正在运行的计算机来说,可能是致命的。
而想要消除死锁,那么我们需要先检测死锁发生的位置,于是就有了死锁检测算法

使用银行家算法中的全局变量

死锁检测算法

手动运行一下好了。

死锁检测时刻

什么时候进行死锁检测主要取决于死锁发生的频率以及死锁涉及的进程数

就像是我们写代码的时候一样,右下角QQ消息时不时的闪动,总是看他,写代码的效率自然不高。

通常情况下我们会选择进程等待时进行检测资源利用率减少时检测以及定时检测。可以看出,前两者都是死锁发生时会带来的问题,出现这种问题再去检测会比较省事儿。

死锁恢复

既然检测到了死锁,那么随后需要做的就是消除死锁。
最简单的消除死锁就是重启系统。但是重启会导致当前进程的工作全部无效,重启之后需要重新执行。

有时候别人说电脑有问题,让我帮忙看看……一般我都是重启一下,然后就可以走了。 ヽ( ̄▽ ̄)ノ
问题:怎么连不上网?
答:重启。
问题:这个软件打不开啊?
答:重启。
……

和重启工作原理相类似的是终止进程,是手动一项一项的终止可能发生死锁的进程。
可以说重启只是此方案的特例。

再有就是操作系统要做的事情了剥夺资源,逐步或一次性剥夺死锁进程的资源,来解除死锁。

进程回退,即让参与死锁的进程回退到之前没有发生死锁的状态,听起来挺简单的,但实际上比较麻烦,而且不容易实现,因为我们不知道何时发生死锁,那么想要回到死锁前的状态,则必须保存进程每一步的状态,这样才能在死锁发生的时候立刻回到前一状态。
开销好大。

当然,如果死锁发生的频度低或者是在可以忍受的范围之内,那么我们可以无视死锁啊,这就是鸵鸟算法的处理方法。

用电脑的时候,突然出现某个窗口无响应,难不成还重启一下吗?一般不都是等一会就好了?

小结


以上有关死锁的问题基本都是针对可复用性资源,这是操作系统管理的主要资源。与之相对的还有消耗型资源,一次性的资源,如消息资源,一般不能采用本文所介绍的策略,但是解决起来却要容易不少,真幸运呐……

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
死锁是指两个或多个进程在互斥地请求资源的时候,由于资源被占用而无法继续执行,导致所有进程都被阻塞的情况。在Spring Boot中,死锁可能发生在多线程并发访问共享资源时,例如数据库连接池、缓存、锁等。 在给出解决方案之前,我们需要先排查死锁问题。一种常用的排查死锁问题的方法是使用jstack命令输出线程的堆栈信息。通过查看堆栈信息,我们可以定位到可能引起死锁的代码行,并进行解决修复。 下面是一个示例代码,模拟了一个可能导致死锁的场景: ```java package com.xz.springboottest.day1; public class DeadLock { private static final String ACTION_ONE = "拿起碗"; private static final String ACTION_TWO = "拿起筷子"; public static void main(String[] args) { // 哲学家小明 new Thread(() -> { synchronized (ACTION_ONE) { try { Thread.sleep(1000); synchronized (ACTION_TWO) { System.out.println("小明开始吃饭了。。。。。。"); } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // 哲学家小张 new Thread(() -> { synchronized (ACTION_TWO) { try { Thread.sleep(1000); synchronized (ACTION_ONE) { System.out.println("小张开始吃饭了。。。。。。"); } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } } ``` 以上代码模拟了哲学家就餐的场景,当只剩下一只碗和一双筷子时,可能出现死锁的问题。 对于解决死锁的方法,可以考虑以下几种方案: 1. 避免循环等待:为了避免死锁,可以规定所有线程在请求资源时按照固定的顺序获取,从而避免循环等待。 2. 加锁顺序:在多个线程同时请求多个资源的情况下,为了避免死锁,可以约定线程必须按照相同的顺序请求资源。 3. 设置超时时间:在获取锁的时候设置超时时间,如果超过一定时间还未获取到锁,可以放弃或者重试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值