死锁面试题(史上最强)


系列:若是整个 地表最强 的开发环境?

工欲善其事 必先利其器
地表最强 开发环境: vagrant+java+springcloud+redis+zookeeper镜像下载(&制做详解)
地表最强 热部署:java SpringBoot SpringCloud 热部署 热加载 热调试
地表最强 发请求工具(再见吧, PostMan ):IDEA HTTP Client(史上最全)
地表最强 PPT 小工具: 屌炸天,像写代码同样写PPT
无编程不创客,疯狂创客圈,一大波编程高手正在交流、学习中!,GO

疯狂创客圈 springCloud 高并发系列

推荐阅读
nacos 实战(史上最全)
sentinel (史上最全+入门教程)
springcloud + webflux 高并发实战
Webflux(史上最全)
SpringCloud gateway (史上最全)
无编程不创客,疯狂创客圈,一大波编程高手正在交流、学习中!,GO

导读:

首先介绍大厂的死锁面试题,而后对死锁作一个全面的解读。web

大厂的死锁面试题

什么是死锁?

所谓死锁,是指多个进程在运行过程当中因争夺资源而形成的一种僵局,当进程处于这种僵持状态时,若无外力做用,它们都将没法再向前推动。 所以咱们举个例子来描述,若是此时有一个线程A,按照先锁a再得到锁b的的顺序得到锁,而在此同时又有另一个线程B,按照先锁b再锁a的顺序得到锁。以下图所示:面试

img

产生死锁的缘由?

可归结为以下两点:redis

a. 竞争资源算法

  • 系统中的资源能够分为两类:
  1. 可剥夺资源,是指某进程在得到这类资源后,该资源能够再被其余进程或系统剥夺,CPU和主存均属于可剥夺性资源;
  2. 另外一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
  • 产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
  • 产生死锁中的竞争资源另一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),一般消息通讯顺序进行不当,则会产生死锁

b. 进程间推动顺序非法spring

  • 若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,由于这两个进程再向前推动,即可能发生死锁
  • 例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,因而发生进程死锁

死锁产生的4个必要条件?

产生死锁的必要条件:编程

  1. 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  2. 请求和保持条件:当进程因请求资源而阻塞时,对已得到的资源保持不放。
  3. 不剥夺条件:进程已得到的资源在未使用完以前,不能剥夺,只能在使用完时由本身释放。
  4. 环路等待条件:在发生死锁时,必然存在一个进程--资源的环形链。

解决死锁的基本方法

1、预防死锁:

  • 资源一次性分配:一次性分配全部资源,这样就不会再有请求了:(破坏请求条件)
  • 只要有一个资源得不到分配,也不给这个进程分配其余的资源:(破坏请保持条件)
  • 可剥夺资源:即当某进程得到了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
  • 资源有序分配法:系统给每类资源赋予一个编号,每个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

1 以肯定的顺序得到锁

若是必须获取多个锁,那么在设计的时候须要充分考虑不一样线程以前得到锁的顺序。按照上面的例子,两个线程得到锁的时序图以下:安全

img

若是此时把得到锁的时序改为:

img

那么死锁就永远不会发生。 针对两个特定的锁,开发者能够尝试按照锁对象的hashCode值大小的顺序,分别得到两个锁,这样锁老是会以特定的顺序得到锁,那么死锁也不会发生。问题变得更加复杂一些,若是此时有多个线程,都在竞争不一样的锁,简单按照锁对象的hashCode进行排序(单纯按照hashCode顺序排序会出现“环路等待”),可能就没法知足要求了,这个时候开发者可使用银行家算法,全部的锁都按照特定的顺序获取,一样能够防止死锁的发生,该算法在这里就再也不赘述了,有兴趣的能够自行了解一下。

2 超时放弃

当使用synchronized关键词提供的内置锁时,只要线程没有得到锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法能够按照固定时长等待锁,所以线程能够在获取锁超时之后,主动释放以前已经得到的全部的锁。经过这种方式,也能够颇有效地避免死锁。 仍是按照以前的例子,时序图以下:

img

2、避免死锁:

  • 预防死锁的几种策略,会严重地损害系统性能。所以在避免死锁时,要施加较弱的限制,从而得到 较满意的系统性能。因为在避免死锁的策略中,容许进程动态地申请资源。于是,系统在进行资源分配以前预先计算资源分配的安全性。若这次分配不会致使系统进入不安全的状态,则将资源分配给进程;不然,进程等待。其中最具备表明性的避免死锁算法是银行家算法。
  • 银行家算法:首先须要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源状况。所以,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会致使死锁。当进程请求一组资源时,假设赞成该请求,从而改变了系统的状态,而后肯定其结果是否还处于安全状态。若是是,赞成这个请求;若是不是,阻塞该进程知道赞成该请求后系统状态仍然是安全的。

3、检测死锁

  1. 首先为每一个进程和每一个资源指定一个惟一的号码;

  2. 而后创建资源分配表和进程等待表。

    死锁检测的工具

    一、Jstack命令

    jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具能够用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的缘由,如线程间死锁、死循环、请求外部资源致使的长时间等待等。 线程出现停顿的时候经过jstack来查看各个线程的调用堆栈,就能够知道没有响应的线程到底在后台作什么事情,或者等待什么资源。

    二、JConsole工具

    Jconsole是JDK自带的监控工具,在JDK/bin目录下能够找到。它用于链接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。并且自己占用的服务器内存很小,甚至能够说几乎不消耗。

4、解除死锁:

当发现有进程死锁后,便应当即把它从死锁状态中解脱出来,常采用的方法有:

  • 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
  • 撤消进程:能够直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

ok,介绍大厂的死锁面试题以后,接下来,对死锁作一个全面的解读。

1、什么是死锁

多线程以及多进程改善了系统资源的利用率并提升了系统 的处理能力。然而,并发执行也带来了新的问题——死锁。
死锁是指两个或两个以上的进程(线程)在运行过程当中因争夺资源而形成的一种僵局(Deadly-Embrace) ) ,若无外力做用,这些进程(线程)都将没法向前推动。

下面咱们经过一些实例来讲明死锁现象。

先看生活中的一个实例,2我的一块儿吃饭可是只有一双筷子,2人轮流吃(同时拥有2只筷子才能吃)。某一个时候,一个拿了左筷子,一人拿了右筷子,2我的都同时占用一个资源,等待另外一个资源,这个时候甲在等待乙吃完并释放它占有的筷子,同理,乙也在等待甲吃完并释放它占有的筷子,这样就陷入了一个死循环,谁也没法继续吃饭。。。
在计算机系统中也存在相似的状况。例如,某计算机系统中只有一台打印机和一台输入设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2 所占用,而P2在未释放打印机以前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均没法继续执行,此时两个进程陷入死锁状态。

关于死锁的一些结论:

  • 参与死锁的进程数至少为两个
  • 参与死锁的全部进程均等待资源
  • 参与死锁的进程至少有两个已经占有资源
  • 死锁进程是系统中当前进程集合的一个子集
  • 死锁会浪费大量系统资源,甚至致使系统崩溃。

举一个例子:

如何解决上面的问题呢?正所谓知己知彼方能百战不殆,咱们要先了解什么状况会发生死锁,才能知道如何避免死锁,很幸运咱们能够站在巨人的肩膀上看待问题

一个银行转帐经典案例:

帐户 A 给帐户 B 转帐,帐户 A 余额减小 100 元,帐户 B 余额增长 100 元,这个操做要是原子性的

先来看程序:

class Account {
  private int balance;
  // 转帐
  synchronized void transfer(
      Account target, int amt){
    if (this.balance > amt) {
      this.balance -= amt;
      target.balance += amt;
    }
  } 
}

用 synchronized 直接保护 transfer 方法,而后操做 资源「Account 的A 余额」和资源「Account的B 余额」就能够了

其中有两个问题:

  1. 单纯的用 synchronized 方法起不到保护做用(不能保护 target)
  2. 用 Account.class 锁方案,锁的粒度又过大,致使涉及到帐户的全部操做(取款,转帐,修改密码等)都会变成串行操做

如何解决这两个问题呢?我们先换好衣服穿越回到过去寻找一下钱庄,一块儿透过现象看本质,dengdeng deng.......

在这里插入图片描述

来到钱庄,告诉柜员你要给铁蛋儿转 100 铜钱,这时柜员转身在墙上寻找你和铁蛋儿的帐本,此时柜员可能面临三种状况:

  1. 理想状态: 你和铁蛋儿的帐本都是空闲状态,一块儿拿回来,在你的帐本上减 100 铜钱,在铁蛋儿帐本上加 100 铜钱,柜员转身将帐本挂回到墙上,完成你的业务
  2. 尴尬状态: 你的帐本在,铁蛋儿的帐本被其余柜员拿出去给别人转帐,你要等待其余柜员把铁蛋儿的帐本归还
  3. 抓狂状态: 你的帐本不在,铁蛋儿的帐本也不在,你只能等待两个帐本都归还

放慢柜员的取帐本操做,他必定是先拿到你的帐本,而后再去拿铁蛋儿的帐本,两个帐本都拿到(理想状态)以后才能完成转帐,用程序模型来描述一下这个拿取帐本的过程:

咱们继续用程序代码描述一下上面这个模型:

class Account {
  private int balance;
  // 转帐
  void transfer(Account target, int amt){
    // 锁定转出帐户
    synchronized(this) {              
      // 锁定转入帐户
      synchronized(target) {           
        if (this.balance > amt) {
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}

这个解决方案看起来很完美,解决了文章开头说的两个问题,但真是这样吗?

咱们刚刚说过的理想状态是钱庄只有一个柜员(既单线程)。随着钱庄规模变大,墙上早已挂了很是多个帐本,钱庄为了应对繁忙的业务,开通了多个窗口,此时有多个柜员(多线程)处理钱庄业务。

柜员 1 正在办理给铁蛋儿转帐的业务,但只拿到了你的帐本;柜员 2 正在办理铁蛋儿给你转帐的业务,但只拿到了铁蛋儿的帐本,此时双方出现了尴尬状态,两位柜员都在等待对方归还帐本为当前客户办理转帐业务。

在这里插入图片描述

现实中柜员会沟通,喊出一嗓子 老铁,铁蛋儿的帐本先给我用一下,用完还给你,但程序却没这么智能,synchronized 内置锁很是执着,它会告诉你「死等」的道理,最终出现死锁

如何解决死锁呢?

Java 有了 synchronized 内置锁,还发明了显示锁 Lock,是否是就为了治一治 synchronized 「死等」的执着呢??

若是你捉急,能够直接去阅读第四节。

2、死锁与饥饿

饥饿(Starvation)指一个进程一直得不到资源。

死锁和饥饿都是因为进程竞争资源而引发的。饥饿通常不占有资源,死锁进程必定占有资源。

3、资源的类型

3.1 可重用资源和消耗性资源

3.1.1 可重用资源(永久性资源)

可被多个进程屡次使用,如全部硬件。

  • 只能分配给一个进程使用,不容许多个进程共享。
  • 进程在对可重用资源的使用时,须按照请求资源、使用资源、释放资源这样的顺序。
  • 系统中每一类可重用资源中的单元数目是相对固定的,进程在运行期间,既不能建立,也不能删除。

3.1.2 消耗性资源(临时性资源)

又称临时性资源,是由进程在运行期间动态的建立和消耗的。

  • 消耗性资源在进程运行期间是能够不断变化的,有时可能为0。
  • 进程在运行过程当中,能够不断地创造可消耗性资源的单元,将它们放入该资源类的缓冲区中,以增长该资源类的单元数目。
  • 进程在运行过程当中,能够请求若干个可消耗性资源单元,用于进程本身消耗,再也不将它们返回给该资源类中。

可消耗资源一般是由生产者进程建立,由消费者进程消耗。最典型的可消耗资源是用于进程间通讯的消息。

3.2 可抢占资源和不可抢占资源

3.2.1 可抢占资源

可抢占资源指某进程在得到这类资源后,该资源能够再被其余进程或系统抢占。对于这类资源是不会引发死锁的。

CPU 和主存均属于可抢占性资源。

3.2.2 不可抢占资源

一旦系统把某资源分配给该进程后,就不能将它强行收回,只能在进程用完后自行释放。

磁带机、打印机等属于不可抢占性资源。

4、死锁产生的缘由

  • 竞争不可抢占资源引发死锁
    一般系统中拥有的不可抢占资源,其数量不足以知足多个进程运行的须要,使得进程在运行过程当中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可抢占资源的竞争 才可能产生死锁,对可抢占资源的竞争是不会引发死锁的。

  • 竞争可消耗资源引发死锁

  • 进程推动顺序不当引发死锁
    进程在运行过程当中,请求和释放资源的顺序不当,也一样会致使死锁。例如,并发进程 P一、P2分别保持了资源R一、R2,而进程P1申请资源R2,进程P2申请资源R1时,二者都会由于所需资源被占用而阻塞。
    信号量使用不当也会形成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间没法继续向前推动。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,能够看出进程A和B不是由于竞争同一资源,而是在等待对方的资源致使死锁。

4.1 竞争不可抢占资源引发死锁

如:共享文件时引发死锁
系统中拥有两个进程P1和P2,它们都准备写两个文件F1和F2。而这二者都属于可重用和不可抢占性资源。若是进程P1在打开F1的同时,P2进程打开F2文件,当P1想打开F2时因为F2已结被占用而阻塞,当P2想打开1时因为F1已结被占用而阻塞,此时就会无线等待下去,造成死锁。
这里写图片描述

4.2 竞争可消耗资源引发死锁

如:进程通讯时引发死锁
系统中拥有三个进程P一、P2和P3,m一、m二、m3是3可消耗资源。进程P1一方面产生消息m1,将其发送给P2,另外一方面要从P3接收消息m3。而进程P2一方面产生消息m2,将其发送给P3,另外一方面要从P1接收消息m1。相似的,进程P3一方面产生消息m3,将其发送给P1,另外一方面要从P2接收消息m2。
若是三个进程都先发送本身产生的消息后接收别人发来的消息,则能够顺利的运行下去不会产生死锁,但要是三个进程都先接收别人的消息而不产生消息则会永远等待下去,产生死锁。
这里写图片描述

4.3 进程推动顺序不当引发死锁

这里写图片描述
上图中,若是按曲线1的顺序推动,两个进程可顺利完成;若是按曲线2的顺序推动,两个进程可顺利完成;若是按曲线3的顺序推动,两个进程可顺利完成;若是按曲线4的顺序推动,两个进程将进入不安全区D中,此时P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,若是继续向前推动,则可能产生死锁。

5、产生死锁的四个必要条件

Coffman 总结出了四个条件说明能够发生死锁的情形:

Coffman 条件

互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。若是此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

不可剥夺条件:指进程已得到的资源,在未使用完以前,不能被剥夺,只能在使用完时由本身释放。

请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对本身已得到的其它资源保持不放。

环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P1,P2,···,Pn}中的 P1 正在等待一个 P2 占用的资源;P2 正在等待 P3 占用的资源,……,Pn 正在等待已被 P0 占用的资源。

在这里插入图片描述

这几个条件很好理解,其中「互斥条件」是并发编程的根基,这个条件没办法改变。但其余三个条件都有改变的可能,也就是说破坏另外三个条件就不会出现上面说到的死锁问题

5.1 互斥条件:

进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时如有其余进程请求该资源,则请求进程只能等待。

5.2 不可剥夺条件:

进程所得到的资源在未使用完毕以前,不能被其余进程强行夺走,即只能由得到该资源的进程本身来释放(只能是主动释放)。

5.3 请求与保持条件:

进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其余进程占有,此时请求进程被阻塞,但对本身已得到的资源保持不放。

5.4 循环等待条件:

存在一种进程资源的循环等待链,链中每个进程已得到的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有,如图2-15所示。

直观上看,循环等待条件彷佛和死锁的定义同样,其实否则。按死锁定义构成等待环所 要求的条件更严,它要求Pi等待的资源必须由P(i+1)来知足,而循环等待条件则无此限制。 例如,系统中有两台输出设备,P0占有一台,PK占有另外一台,且K不属于集合{0, 1, …, n}。

Pn等待一台输出设备,它能够从P0得到,也可能从PK得到。所以,虽然Pn、P0和其余 一些进程造成了循环等待圈,但PK不在圈内,若PK释放了输出设备,则可打破循环等待, 如图2-16所示。所以循环等待只是死锁的必要条件。

这里写图片描述

资源分配图含圈而系统又不必定有死锁的缘由是同类资源数大于1。但若系统中每类资 源都只有一个资源,则资源分配图含圈就变成了系统出现死锁的充分必要条件。

以上这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不知足,就不会发生死锁。

产生死锁的一个例子:

/**
 * 一个简单的死锁类
 * 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
 * 而td1在睡眠的时候另外一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
 * td1睡眠结束后须要锁定o2才能继续执行,而此时o2已被td2锁定;
 * td2睡眠结束后须要锁定o1才能继续执行,而此时o1已被td1锁定;
 * td一、td2相互等待,都须要获得对方锁定的资源才能继续执行,从而死锁。
 */
public class DeadLock implements Runnable {
    public int flag = 1;  
    //静态对象是类的全部对象共享的  
    private static Object o1 = new Object(), o2 = new Object();  
    @Override  
    public void run() {  
        System.out.println("flag=" + flag);  
        if (flag == 1) {  
            synchronized (o1) {  
                try {  
                    Thread.sleep(500);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                synchronized (o2) {  
                    System.out.println("1");  
                }  
            }  
        }  
        if (flag == 0) {  
            synchronized (o2) {  
                try {  
                    Thread.sleep(500);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                synchronized (o1) {  
                    System.out.println("0");  
                }  
            }  
        }  
    }  

    public static void main(String[] args) {
        DeadLock td1 = new DeadLock();
        DeadLock td2 = new DeadLock();
        td1.flag = 1;
        td2.flag = 0;
        //td1,td2都处于可执行状态,但JVM线程调度先执行哪一个线程是不肯定的。  
        //td2的run()可能在td1的run()以前运行  
        new Thread(td1).start();  
        new Thread(td2).start();
    }  
}

6、处理死锁的方法

  • 预防死锁:经过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
  • 避免死锁:在资源的动态分配过程当中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
  • 检测死锁:容许系统在运行过程当中发生死锁,但可设置检测机构及时检测死锁的发生,并采起适当措施加以清除。
  • 解除死锁:当检测出死锁后,便采起适当措施将进程从死锁状态中解脱出来。

7、 预防死锁

经过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生

破坏死锁的条件:

  1. 破坏“互斥”条件:
    就是在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是确定不会发生的。但通常来讲在所列的四个条件中,“互斥”条件是没法破坏的。所以,在死锁预防里主要是破坏其余几个必要条件,而不去涉及破坏“互斥”条件。

    注意:互斥条件不能被破坏,不然会形成结果的不可再现性。

  2. 破坏“占有并等待”条件:
    破坏“占有并等待”条件,就是在系统中不容许进程在已得到某种资源的状况下,申请其余资源。即要想出一个办法,阻止进程在持有资源的同时申请其余资源。
    方法一:建立进程时,要求它申请所需的所有资源,系统或知足其全部要求,或什么也不给它。这是所谓的 “ 一次性分配”方案。
    方法二:要求每一个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在须要资源S时,须先把它先前占有的资源R释放掉,而后才能提出对S的申请,即便它可能很快又要用到资源R。

  3. 破坏“不可抢占”条件:
    破坏“不可抢占”条件就是容许对资源实行抢夺。
    方法一:若是占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,若是有必要,可再次请求这些资源和另外的资源。
    方法二:若是一个进程请求当前被另外一个进程占有的一个资源,则操做系统能够抢占另外一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。

  4. 破坏“循环等待”条件:
    破坏“循环等待”条件的一种方法,是将系统中的全部资源统一编号,进程可在任什么时候刻提出资源申请,但全部申请必须按照资源的编号顺序(升序)提出。这样作就能保证系统不出现死锁。

银行转帐经典案例中的死锁的避免

解决前面的银行转帐经典案例的死锁, 有如下方法

方法一:破坏请求和保持条件

每一个柜员均可以取放帐本,很容易出现互相等待的状况。要想破坏请求和保持条件,就要一次性拿到全部资源。

能够不容许柜员均可以取放帐本,帐本要由单独的帐本管理员来管理

在这里插入图片描述

也就是说帐本管理员拿取帐本是临界区,若是只拿到其中之一的帐本,那么不会给柜员,而是等待柜员下一次询问是否两个帐本都在

//帐本管理员
public class AccountBookManager {
    synchronized boolean getAllRequiredAccountBook( Object from, Object to){
        if(拿到全部帐本){
            return true;
        } else{
            return false;
        }
    }
    // 归还资源
    synchronized void releaseObtainedAccountBook(Object from, Object to){
        归还获取到的帐本
    }
}
 
 
 
 
public class Account {
    //单例的帐本管理员
    private AccountBookManager accountBookManager;
 
 
    public void transfer(Account target, int amt){
        // 一次性申请转出帐户和转入帐户,直到成功
        while(!accountBookManager.getAllRequiredAccountBook(this, target)){
            return;
        }
 
 
        try{
            // 锁定转出帐户
            synchronized(this){
                // 锁定转入帐户
                synchronized(target){
                    if (this.balance > amt){
                        this.balance -= amt;
                        target.balance += amt;
                    }
                }
            }
        } finally {
            accountBookManager.releaseObtainedAccountBook(this, target);
        }
    }
}

方法二:破坏不可剥夺条件

上面已经给了你小小的提示,为了解决内置锁的执着,Java 显示锁支持通知(notify/notifyall)和等待(wait),也就是说该功能能够实现喊一嗓子 老铁,铁蛋儿的帐本先给我用一下,用完还给你 的功能,

还有,能够经过 加锁时限(线程尝试获取锁的时候加上必定的时限,超过期限则放弃对该锁的请求,并释放本身占有的锁)去解决。

下面是一个相似的例子。

在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程当中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功得到全部须要的锁,则会进行回退并释放全部已经得到的锁,而后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,而且让该应用在没有得到锁的时候能够继续运行(译者注:加锁超时后能够先继续运行干点其它事情,再回头来重复以前加锁的逻辑)。

如下是一个例子,展现了两个线程以不一样的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:

Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
Thread 1’s lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.
Thread 2’s lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,线程2比线程1早200毫秒进行重试加锁,所以它能够先成功地获取到两个锁。这时,线程1尝试获取锁A而且处于等待状态。当线程2结束时,线程1也能够顺利的得到这两个锁(除非线程2或者其它线程在线程1成功得到两个锁以前又得到其中的一些锁)。

须要注意的是,因为存在锁的超时,因此咱们不能认为这种场景就必定是出现了死锁。也多是由于得到了锁的线程(致使其它线程超时)须要很长的时间去完成它的任务。

此外,若是有很是多的线程同一时间去竞争同一批资源,就算有超时和回退机制,仍是可能会致使这些线程重复地尝试但却始终得不到锁。若是只有两个线程,而且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,可是若是是10个或20个线程状况就不一样了。由于这些线程等待相等的重试时间的几率就高的多(或者很是接近以致于会出现问题)。
(译者注:超时和重试机制是为了不在同一时间出现的竞争,可是当线程不少时,其中两个或多个线程的超时时间同样或者接近的可能性就会很大,所以就算出现竞争而致使超时后,因为超时时间同样,它们又会同时开始重试,致使新一轮的竞争,带来了新的问题。)

这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。须要使用 JUC 的显式锁。

方法三:破坏环路等待条件

破坏环路等待条件,就是按照相同的顺序得到锁。

若是能确保全部的线程都是按照相同的顺序得到锁,那么死锁就不会发生。看下面这个例子:

Thread 1:
lock A
lock B
Thread 2:
wait for A
lock C (when A locked)
Thread 3:
wait for A
wait for B
wait for C

若是一个线程(好比线程3)须要一些锁,那么它必须按照肯定的顺序获取锁。它只有得到了从顺序上排在前面的锁以后,才能获取后面的锁。

例如,线程2和线程3只有在获取了锁A以后才能尝试获取锁C(译者注:获取锁A是获取锁C的必要条件)。由于线程1已经拥有了锁A,因此线程2和3须要一直等到锁A被释放。而后在它们尝试对B或C加锁以前,必须成功地对A加了锁。

按照顺序加锁是一种有效的死锁预防机制。可是,这种方式须要你事先知道全部可能会用到的锁(译者注:并对这些锁作适当的排序),但总有些时候是没法预知的。

破坏环路等待条件也很简单,咱们只须要将资源序号大小排序获取就会解决这个问题,将环路拆除

在这里插入图片描述

按照id大小的顺序来加锁,先锁住id 小的,而后才锁住 id 大的

class Account {
  private int id;
  private int balance;
  // 转帐
  void transfer(Account target, int amt){
    Account smaller = this        
    Account larger = target;    
    // 排序
    if (this.id > target.id) { 
      smaller = target;           
      larger = this;            
    }                          
    // 锁定序号小的帐户
    synchronized(smaller){
      // 锁定序号大的帐户
      synchronized(larger){ 
        if (this.balance > amt){
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}

当 smaller 被占用时,其余线程就会被阻塞,也就不会存在死锁了.

八 避免死锁

理解了死锁的缘由,尤为是产生死锁的四个必要条件,就能够最大可能地避免、预防和解除死锁。因此,在系统设计、进程调度等方面注意如何让这四个必要条件不成立,如何肯定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的状况下占用资源。所以,对资源的分配要给予合理的规划。

预防死锁和避免死锁的区别:
预防死锁是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现,而避免死锁则不那么严格的限制产生死锁的必要条件的存在,由于即便死锁的必要条件存在,也不必定发生死锁。避免死锁是在系统运行过程当中注意避免死锁的最终发生。

经常使用避免死锁的方法

有序资源分配法

这种算法资源按某种规则系统中的全部资源统一编号(例如打印机为一、磁带机为二、磁盘为三、等等),申请时必须以上升的次序。系统要求申请进程:
  一、对它所必须使用的并且属于同一类的全部资源,必须一次申请完;
  二、在申请不一样类资源时,必须按各种设备的编号依次申请。例如:进程PA,使用资源的顺序是R1,R2; 进程PB,使用资源的顺序是R2,R1;若采用动态分配有可能造成环路条件,形成死锁。
  采用有序资源分配法:R1的编号为1,R2的编号为2;
  PA:申请次序应是:R1,R2
  PB:申请次序应是:R1,R2
  这样就破坏了环路条件,避免了死锁的发生。
  

银行家算法

详见银行家算法.

九 检测死锁

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁而且锁超时也不可行的场景。

每当一个线程得到了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此以外,每当有线程请求锁,也须要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程能够遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,可是锁7这个时候被线程B持有,这时线程A就能够检查一下线程B是否已经请求了线程A当前所持有的锁。若是线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

固然,死锁通常要比两个线程互相持有对方的锁这种状况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它须要递进地检测全部被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,而后又找到了线程D,发现线程D请求的锁被线程A本身持有着。这是它就知道发生了死锁。

下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就能够被用来检测死锁。

这里写图片描述

通常来讲,因为操做系统有并发,共享以及随机性等特色,经过预防和避免的手段达到排除死锁的目的是很困难的。这须要较大的系统开销,并且不能充分利用资源。为此,一种简便的方法是系统为进程分配资源时,不采起任何限制性措施,可是提供了检测和解脱死锁的手段:能发现死锁并从死锁状态中恢复出来。所以,在实际的操做系统中每每采用死锁的检测与恢复方法来排除死锁。
死锁检测与恢复是指系统设有专门的机构,当死锁发生时,该机构可以检测到死锁发生的位置和缘由,并能经过外力破坏死锁发生的必要条件,从而使得并发进程从死锁状态中恢复出来。
这时进程P1占有资源R1而申请资源R2,进程P2占有资源R2而申请资源R1,按循环等待条件,进程和资源造成了环路,因此系统是死锁状态。进程P1,P2是参与死锁的进程。
下面咱们再来看一看死锁检测算法。算法使用的数据结构是以下这些:
占有矩阵A:nm阶,其中n表示并发进程的个数,m表示系统的各种资源的个数,这个矩阵记录了每个进程当前占有各个资源类中资源的个数。
申请矩阵R:n
m阶,其中n表示并发进程的个数,m表示系统的各种资源的个数,这个矩阵记录了每个进程当前要完成工做须要申请的各个资源类中资源的个数。
空闲向量T:记录当前m个资源类中空闲资源的个数。
完成向量F:布尔型向量值为真(true)或假(false),记录当前n个并发进程可否进行完。为真即能进行完,为假则不能进行完。
临时向量W:开始时W:=T。
算法步骤:
(1)W:=T,
对于全部的i=1,2,…,n,
若是A[i]=0,则F[i]:=true;不然,F[i]:=false
(2)找知足下面条件的下标i:
F[i]:=false而且R[i]〈=W
若是不存在知足上面的条件i,则转到步骤(4)。
(3)W:=W+A[i]
F[i]:=true
转到步骤(2)
(4)若是存在i,F[i]:=false,则系统处于死锁状态,且Pi进程参与了死锁。何时进行死锁的检测取决于死锁发生的频率。若是死锁发生的频率高,那么死锁检测的频率也要相应提升,这样一方面能够提升系统资源的利用率,一方面能够避免更多的进程卷入死锁。若是进程申请资源不能知足就马上进行检测,那么每当死锁造成时即能被发现,这和死锁避免的算法相近,只是系统的开销较大。为了减少死锁检测带来的系统开销,通常采起每隔一段时间进行一次死锁检测,或者在CPU的利用率下降到某一数值时,进行死锁的检测。

十 解除死锁

一旦检测出死锁,就应当即釆取相应的措施,以解除死锁。
死锁解除的主要方法有:

  1. 资源剥夺法。挂起某些死锁进程,并抢占它的资源,将这些资源分配给其余的死锁进程。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态。
  2. 撤销进程法。强制撤销部分、甚至所有死锁进程并剥夺这些进程的资源。撤销的原则能够按进程优先级和撤销进程代价的高低进行。
  3. 进程回退法。让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。

参考文档:

https://cloud.tencent.com/developer/article/1541513

https://blog.csdn.net/fdoubleman/article/details/97238420

http://www.voidcn.com/article/p-gjtwwpmp-ws.html

http://www.javashuo.com/article/p-nsfkkato-eh.html

相关文章
相关标签/搜索