死锁及其预防策略

Posted ronnieyuan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了死锁及其预防策略相关的知识,希望对你有一定的参考价值。

什么是死锁?

  • 如果一个进程集合中的每个进程都在等待只能只能有该集合中的其他一个进程才能引发的事件, 这种情况就是死锁。

  • 简单举例

    • 资源 A 与 资源 B 都是不可剥夺资源

    • 进程 C 已经申请到资源A, 进程D已经申请到了资源B

    • 进程 C 此时申请资源B, 而进程D恰好申请了资源A

    • 由于资源已被占用, 进程A和进程B都无法执行下一步操作, 就造成了死锁。

      技术图片

产生死锁的四个必要条件

  • 互斥条件 (Mutual exclusive)
    • 资源不能被共享, 只能由一个进程使用。
  • 请求与保持条件 (Hold and Wait)
    • 已经得到资源的进程可以再次申请新的资源。
  • 非剥夺条件 (No pre-emption)
    • 已经分配的资源不能从相应的进程强制地剥夺。
  • 循环等待条件 (Circular wait)
    • 系统中若干进程组成环路, 该环路中每个进程都在等待相邻进程整占用的资源。

死锁处理策略

  • 预防死锁

    • 设置某些限制条件, 破坏产生死锁的四个必要条件中的一个或几个, 以预防发生死锁。
  • 避免死锁

    • 在资源动态分配中, 用某种方法防止系统进入不安全状态, 从而避免死锁。
  • 死锁 检测 和 解除

    • 无需采取任何限制性措施,允许进程在运行过程中发生死锁。通过系统检测机构及时地检测死锁的发生,然后采取某种措施解除死锁。
    资源分配策略 各种可能模式 主要优点 主要缺点
    死锁预防 保守, 宁可资源限制 一次请求所有的资源, 资源剥夺, 资源按序分配 适用于做突发式处理的进程, 不必进行剥夺。 效率低, 进程初始化时间延长; 剥夺次数过多; 不便灵活申请新资源。
    死锁避免 “预防” 和 “检测” 的折中 寻找可能的安全允许顺序 不必进行剥夺 必须知道将来的资源需求; 进程不能被长时间阻塞。
    死锁检测 宽松, 只要允许就分配资源 定期检查死锁是否已经发生 不延长进程初始化时间, 允许对死锁进行现场处理。 通过剥夺解除死锁造成的损失。
  • 银行家算法:

    • 把操作系统看做是银行家,操作系统管理的资源相当于银行家管理的资金,进程向操作系统请求 分配资源相当于用户向银行家贷款。
    • 操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源, 否则就推迟分配。
    • 当进程在执行中继续申请资源时,先测试该进程已占用的资源数和本次申请的资源数之和是否超过了该进程对资源的最大的需求量。
    • 若超过则拒绝分配资源,过没有超过则再测试系统现存的资源能否满足该进程尚需的最大的资源量,若能满足则按当前的申请分配资源,否则也要推迟。
  • 解除死锁的方法

    • 资源剥夺法:挂起某些死进程并抢夺它的资源,以便让其他进程继续推进
    • 撤销进程法:强制撤销部分,甚至全部死锁进程并剥夺这些进程的资源
    • 进程回退法:让进程回退到足以避免死锁的地步

例题解析

  • Leetcode 1226题, 哲学家进餐

  • 5个沉默寡言的哲学家围坐在圆桌前,每人面前一盘意面。叉子放在哲学家之间的桌面上。(5 个哲学家,5 根叉子)

    所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的叉子才能吃到面,而同一根叉子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把叉子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的叉子,但在没有同时拿到左右叉子时不能进食。

    假设面的数量没有限制,哲学家也能随便吃,不需要考虑吃不吃得下。

    设计一个进餐规则(并行算法)使得每个哲学家都不会挨饿;也就是说,在没有人知道别人什么时候想吃东西或思考的情况下,每个哲学家都可以在吃饭和思考之间一直交替下去。

    技术图片

  • 哲学家从 0 到 4 按 顺时针 编号。请实现函数 void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork):

    • philosopher 哲学家的编号。
    • pickLeftForkpickRightFork 表示拿起左边或右边的叉子。
    • eat 表示吃面。
    • putLeftForkpickRightFork 表示放下左边或右边的叉子。
    • 由于哲学家不是在吃面就是在想着啥时候吃面,所以思考这个方法没有对应的回调。
  • 给你 5 个线程,每个都代表一个哲学家,请你使用类的同一个对象来模拟这个过程。

  • 在最后一次调用结束之前,可能会为同一个哲学家多次调用该函数。

  • 示例:

    输入:n = 1
    输出:[[4,2,1],[4,1,1],[0,1,1],[2,2,1],[2,1,1],[2,0,3],[2,1,2],[2,2,2],[4,0,3],[4,1,2],[0,2,1],[4,2,2],[3,2,1],[3,1,1],[0,0,3],[0,1,2],[0,2,2],[1,2,1],[1,1,1],[3,0,3],[3,1,2],[3,2,2],[1,0,3],[1,1,2],[1,2,2]]
    解释:
    n 表示每个哲学家需要进餐的次数。
    输出数组描述了叉子的控制和进餐的调用,它的格式如下:
    output[i] = [a, b, c] (3个整数)
    - a 哲学家编号。
    - b 指定叉子:{1 : 左边, 2 : 右边}.
    - c 指定行为:{1 : 拿起, 2 : 放下, 3 : 吃面}。
    如 [4,2,1] 表示 4 号哲学家拿起了右边的叉子。 
    • 解法一

      package com.ronnie.leetcode;
      
      import java.util.concurrent.Semaphore;
      import java.util.concurrent.locks.ReentrantLock;
      
      public class DiningPhilosophers {
      
          // 1 fork => 1 ReetrantLock
          private ReentrantLock[] lockList = {
                  new ReentrantLock(),
                  new ReentrantLock(),
                  new ReentrantLock(),
                  new ReentrantLock(),
                  new ReentrantLock()
          };
          // restriction: Only 4 Philosophers can get at least one fork
          private Semaphore eatLimit = new Semaphore(4);
      
          public DiningPhilosophers() {
      
          }
      
          // call the run() method of any runnable to execute its code
          public void wantsToEat(int philosopher,
                                 Runnable pickLeftFork,
                                 Runnable pickRightFork,
                                 Runnable eat,
                                 Runnable putLeftFork,
                                 Runnable putRightFork) throws InterruptedException {
      
              int leftFork = (philosopher+1)%5;
              int rightFork = philosopher;
              eatLimit.acquire();
      
              lockList[leftFork].lock();
              lockList[rightFork].lock();
      
              pickLeftFork.run();
              pickRightFork.run();
      
              eat.run();
      
              putLeftFork.run();
              putRightFork.run();
      
              lockList[leftFork].unlock();
              lockList[rightFork].unlock();
      
              eatLimit.release();
          }
      }
    • 解法二

      package com.ronnie.leetcode;
      
      import java.util.concurrent.locks.ReentrantLock;
      
      public class DiningPhilosophers2 {
      
          // 1 fork => 1 ReentrantLock
          private ReentrantLock[] lockList = {
                new ReentrantLock(),
                new ReentrantLock(),
                new ReentrantLock(),
                new ReentrantLock()
          };
      
          // 思路: 设置一个临界区, 进入临界区后, 只有当哲学家获取到左右两把叉子并执行代码后, 才退出临界区
          private ReentrantLock pickBothForks = new ReentrantLock();
      
          public DiningPhilosophers2(){
      
          }
      
      
          // call the run() method of any runnable to execute its code
          public void wantsToEat(int philosopher,
                                 Runnable pickLeftFork,
                                 Runnable pickRightFork,
                                 Runnable eat,
                                 Runnable putLeftFork,
                                 Runnable putRightFork) throws InterruptedException {
      
                      int leftFork = (philosopher + 1) % 5;
                      int rightFork = philosopher;
      
                      // 进入临界区
                      lockList[leftFork].lock();
                      lockList[rightFork].lock();
      
                      pickLeftFork.run();
                      pickRightFork.run();
      
                      // 退出临界区
                      pickBothForks.unlock();
      
                      putLeftFork.run();
                      pickRightFork.run();
      
                      lockList[leftFork].unlock();
                      lockList[rightFork].unlock();
          }
      }
    • 解法三

      package com.ronnie.leetcode;
      
      import java.util.concurrent.locks.ReentrantLock;
      
      /**
       *  只有当5个哲学家都左手持其左边的叉子 或 都右手持有期右边的叉子时, 才会发送死锁。
       */
      public class DiningPhilosophers3 {
          // 1 fork => 1 ReetrantLock
          private ReentrantLock[] lockList = {
                  new ReentrantLock(),
                  new ReentrantLock(),
                  new ReentrantLock(),
                  new ReentrantLock(),
                  new ReentrantLock()
          };
      
          public DiningPhilosophers3() {
          }
      
          // call the run() method of any runnable to execute its code
          public void wantsToEat(int philosopher,
                                 Runnable pickLeftFork,
                                 Runnable pickRightFork,
                                 Runnable eat,
                                 Runnable putLeftFork,
                                 Runnable putRightFork) throws InterruptedException {
              int leftFork = (philosopher + 1) % 5;
              int rightFork = philosopher;
      
              // 让编号为偶数的哲学家优先拿左边, 再拿右边
              if (philosopher % 2 == 0){
                  lockList[leftFork].lock();
                  lockList[rightFork].lock();
              } else {
                  // 编号为奇数的旧先拿右边再拿左边, 这样就避免了死锁
                  lockList[rightFork].lock();
                  lockList[leftFork].lock();
              }
      
              pickLeftFork.run();
              pickRightFork.run();
      
              eat.run();
      
              putLeftFork.run();
              putRightFork.run();
      
              lockList[leftFork].unlock();
              lockList[rightFork].unlock();
          }
      }
      

以上是关于死锁及其预防策略的主要内容,如果未能解决你的问题,请参考以下文章

死锁及其预防策略

防止代码死锁的锁定策略和技术

java死锁示例及其发现方法

死锁处理策略和死锁预防

死锁的处理策略—预防死锁避免死锁检测和解除死锁

死锁的处理策略—预防死锁避免死锁检测和解除死锁