操作系统之死锁(死锁原因、四个必要条件、如何解锁死锁、银行家算法、哲学家就餐问题)

1. 死锁概述

① 什么是死锁?
  • 官方解释: 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源由于彼此通信而造成的一种阻塞现象。若无外力作用,他们都将无法推进下去。
  • 自己的理解:
  1. 当前进程拥有其他进程等待的资源
  2. 当前进程等待其他进程已拥有的资源
  3. 它们都不放弃自己拥有的资源
  • 死锁实例: 十字路口的汽车,互相等待其他汽车释放资源,却又不愿意退出路口。
    操作系统之死锁(死锁原因、四个必要条件、如何解锁死锁、银行家算法、哲学家就餐问题)_第1张图片
② 死锁产生的原因
  • 根据死锁的定义,死锁产生的原因如下:
  1. 由于资源不足而导致的资源竞争: 多个进程所共享的资源不足,引起他们对资源的竞争而产生死锁。
  2. 由于并发执行的顺序不当: 进程运行过程中,请求和释放资源的顺序不当而导致进程死锁。
③ 死锁的程序实例
  • 线程1和线程2均需要获取对象A和B,线程1先获取对象A,再获取对象B;线程2先获取对象B,再获取对象A。双方都在等地对方释放已拥有的对象,且不放弃自己已拥有的对象。
public class DeadLock {
    public static String objA = "objA";
    public static Integer objB = 1;
    public static void main(String[] arg) {
        Thread1 thread1=new Thread1();
        Thread th1=new Thread(thread1);
        Thread2 thread2=new Thread2();
        Thread th2=new Thread(thread2);
        th1.start();
        th2.start();
    }
}
class Thread1 implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("Thread1 is running ...");
            synchronized (DeadLock.objA) {// Thread1先获取objA
                System.out.println("Thread1 got the DeadLock.objA");
                Thread.sleep(3000); // 等待Thread2获取资源objB
                synchronized (DeadLock.objB){// Thread1再获取objB
                    System.out.println("Thread1 got the DeadLock.objB");
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class Thread2 implements Runnable{
    @Override
    public void run() {
        try {
            System.out.println("Thread2 is running ...");
            synchronized (DeadLock.objB){// Thread2先获取objB
                System.out.println("Thread2 got the DeadLock.objB");
                Thread.sleep(3000);// 等待Thread1获取资源objA
                synchronized (DeadLock.objA){// Thread2再获取objA
                    System.out.println("Thread2 got the DeadLock.objA");
                }
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}

操作系统之死锁(死锁原因、四个必要条件、如何解锁死锁、银行家算法、哲学家就餐问题)_第2张图片

④ 资源分配图
  • 资源分配图是有向图,其中进程用圆圈表示资源用方框表示,方框中的点表示资源的数量。
  • 进程指向资源,表示进程请求资源;资源指向进程,表示进程占有资源。
    在这里插入图片描述
  • 存在死锁的资源分配图:
    操作系统之死锁(死锁原因、四个必要条件、如何解锁死锁、银行家算法、哲学家就餐问题)_第3张图片
  • 注意:
  1. 资源分配图存在环,并不一定产生死锁;产生死锁,资源分配图必定存在环。
  2. 需要简化进程之间的资源等待关系,看是否存在死循环等待。
⑤ 产生死锁的四个必要条件
  • 互斥: 一个资源一次只能分配给一个进程使用。
  • 占有且等待: 一个进程在等待其他进程释放资源的同时,继续占有已经拥有的资源。
  • 不可抢占: 一个进程不能强行占有其他进程已占有的资源。
  • 循环等待: 若干进程由于等待其他进程释放资源,而形成相互等待的循环。如进程A等待进程B,进程B等待进程C,···,进程X等待进程Y,进程Y等待进程A。
  • 说明:
  1. 互斥、占有且等待、不可抢占是形成死锁的必要条件;互斥、占有且等待、不可抢占、循环等待是形成死锁的充分条件
  2. 循环等待是前三个条件的潜在结果,也是死锁的定义。

2. 如何解决死锁?

  • 解决死锁的四种办法:鸵鸟策略、死锁的预防、死锁的避免、死锁的检测与恢复。
① 鸵鸟策略
  • 鸵鸟策略:把头埋在沙子里,假装根本没有发生问题。
  • 为什么可以采用鸵鸟策略?
  1. 解决死锁的代价很高,采用鸵鸟策略:不采取任何措施,能获得更高的性能。
  2. 死锁发生的概率很低,就算发生死锁对对用户的影响并不大,所以可以采用鸵鸟策略。
  • 大多数操作系统,包括Linux、Unix和Windows,解决死锁的办法仅仅是忽略它。#
② 死锁的预防
  • 死锁的预防是指采取一些措施破坏死锁发生的必要条件让死锁无法发生
  • 死锁预防的方法:
  1. 间接方法: 防止前面三个必要条件中任意一个的发生。,
  2. 直接方法: 防止循环等待的发生。
  • 破坏互斥
  1. 互斥不可能被禁止: 如文件,可以允许多个读访问,但不能允许多个写访问。
  • 破坏占有且等待
  1. 要求进程在运行之前一次性获取所有需要的资源,并且阻塞进程直到可以一次性获取所有需要的资源。
  2. 该方法存在两个缺陷: 低效;一个进程可能无法事先知道它需要哪些资源。
  3. 低效表现1: 一个进程在能一次性获取所有需要资源前,可能被阻塞很长时间。
  4. 低效表现2: 一个进程一次性获取到所有需要的资源后,有的资源可能在很长一段时间不会使用,而其他进程也无法使用,导致资源利用率降低。
  • 破坏不可抢占
  1. 方法一: 一个进程占有某些资源后进步申请其他资源,如果遭到拒绝,该进程必须释放已经占有的资源。
  2. 方法二: 在两个进程优先级不同的情况下,高优先级的进程申请被低优先级进程占有的资源时,系统要求低优先级的进程释放资源。
  • 破坏循环等待
  1. 给资源统一进行编号,进程只能按照编号顺序请求资源。
  2. 如上面发生死锁的实例中,objA的编号< objB的编号,两个线程都因该先获取objA的锁,再获取objB的锁。
③ 死锁的避免
  • 死锁的避免是指在资源的动态分配过程中,采取一些算法防止系统进入不安全状态,从而避免死锁的发生。它不需要事先破坏死锁发生的必要条件。
  • 死锁的避免仅仅是预测发生死锁的可能性,并确保不会出现这种可能性。(有苗头就扼杀
  • 死锁避免的两种方法:
  1. 进程启动拒绝: 如果一个进程的资源请求会导致死锁,则不启动该进程。
  2. 资源分配拒绝(银行家算法): 如果一个进程增加资源的请求会导致死锁,则不允许此分配。
  • n个进程m种资源的前提下,相关的概念:
  1. 向量R表示每种资源的总量(Resource), R = ( R 1 , R 2 , . . . , R m ) R=(R_1,R_2,...,R_m) R=(R1,R2,...,Rm)
  2. 向量V表示每种资源的剩余量(Available), V = ( V 1 , v 2 , . . . , V m ) V=(V_1,v_2,...,V_m) V=(V1,v2,...,Vm)
  3. Claim矩阵, C i j C_{ij} Cij表示进程i对资源j的需求
  4. Allocation矩阵, A i j A_{ij} Aij表示进程i已经分配到的资源j的数量
  • 进程启动拒绝:
  1. n+1个进程对资源j的需求,应该满足:前n个进程的最大资源需求加上当第n+1个进程的资源需求,应该小于等于总资源量。
    C ( n + 1 ) j + ∑ i n C i j ≤ R j C_{(n+1)j}+\sum_{i}^{n}C_{ij} \le R_j C(n+1)j+inCijRj
  2. 该方法很难是最优的,因为它总是假设最坏的情况: 所有的进程同时发出其最大资源请求。
  • 资源分配拒绝:
  1. 安全状态safe state):当进程发出增加资源的请求时,至少有一种资源分配序列不会导致死锁。即至少有一种资源分配序列,能保证所有的进程顺利运行直到结束。
  2. 不安全状态: 不存在任何一种资源分配序列保证不会死锁。
  3. 举例: 当进程P1P2P3都发起增加资源的请求时,可以先满足进程P2的资源请求;当进程P2运行结束后,释放资源,可用资源增加;接着满足进程P1的资源请求,最后满足进程P3的资源请求。
  4. 定义矩阵 C − A C-A CA,即矩阵C和矩阵A中对应位置的数值相减,记录进程对每种资源的剩余需求。
  5. 对于进程i,它对资源j增加的请求都满足: C i j − A i j ≤ V i j C_{ij} - A{ij} \le V_{ij} CijAijVij,即需要的量小于等于现存量
    操作系统之死锁(死锁原因、四个必要条件、如何解锁死锁、银行家算法、哲学家就餐问题)_第4张图片
    操作系统之死锁(死锁原因、四个必要条件、如何解锁死锁、银行家算法、哲学家就餐问题)_第5张图片
    操作系统之死锁(死锁原因、四个必要条件、如何解锁死锁、银行家算法、哲学家就餐问题)_第6张图片
    操作系统之死锁(死锁原因、四个必要条件、如何解锁死锁、银行家算法、哲学家就餐问题)_第7张图片
⑤ 死锁的检测与恢复
  • 死锁的检测与恢复: 操作系统周期性地执行算法检测是有存在循环等待,如果存在,则对死锁进行恢复。
  • 自己的理解:不是试图阻止死锁,而是检测死锁的发生并进行恢复。
  • 每种类型一个资源的的死锁检测:
  1. 利用资源分配图,检测有向图中是否存在环。如果存在环,表明死锁已经发生,采取措施进行恢复。
  2. 具体实现: 从一个节点出发进行深度优先搜索DFS),对访问过的节点进行标记。如果访问了已经标记的节点,就表示有向图存在环,即检测到死锁的发生。
  • 每种类型多个资源的死锁检测:
  1. 需要使用到死锁避免中的分配矩阵A,新添请求矩阵Q表示每个进程将要请求的资源数量。
  2. 首先标记矩阵A中全为0的行对应的进程。
  3. 查找下标i,要求进程Pi未被标记且矩阵Q中对应的行满足: Q i ≤ V Q_i \le V QiV。如果找不到这样的下标i,则终止算法。
  4. 找到下标i,将进程Pi已分配的资源加到V中,并标记进程Pi。重复步骤3和4。
  5. 算法终止时,如果存在未被标记的进程,说明存在死锁。
  • 死锁的恢复:
  1. 取消所有死锁的进程。这时操作系统中最常使用的方法。
  2. 把每个死锁的进程回滚到前面定义的某些检查点(checkpoint),重新启动所有进程。虽然存在原来的死锁再次发生的风险,但是并发的不确定性通常能保证这种风险不会出现。
  3. 连续取消死锁的进程直到不存在死锁。 取消的顺序基于某种代价最小原则,每取消一个进程都需要重新执行死锁检测算法,以检查是否存在死锁。
  4. 连续抢占资源直到不存在死锁。 抢占的顺序基于某种代价的选择方法,每次抢占时,被抢占资源的进程需要回滚到获得这些资源之前的状态,还需要重新执行死锁检测算法。

3. 银行家算法

① 什么是银行家算法?
  • 实际场景描述:
  1. 小镇上有一个银行家,他有一群客户。只有当客户的贷款请求被满足,客户才能成功投资并还款。否则,银行家借出去的钱,永远无法收回。
  • 银行家为了安抚客户,只会先允许一部分的贷款请求。这时,银行家手里的资金变得不充裕。
  • 如果他能找到一种将剩余资金进行分配的有效方法,使得所有的客户的贷款请求都满足,他才能收回自己的钱。
  • 银行家算法要做的事情: 判断对当前请求的满足,是否会导致银行家进入不安全状态。如果会,则拒绝请求;如果不会,则满足请求。
② 银行家算法的过程
  • 如何检查一个状态是否安全?
  1. 初始化:根据矩阵C和A,计算出对应矩阵 C − A C-A CA
  2. 查找矩阵 C − A C-A CA中是否存在一行小于向量V。如果找不到,那么系统将会发生死锁,状态是不安全的
  3. 如果找到一行,则将其已分配的资源加入到向量V中,并标记该进程终止:更新矩阵C、A、矩阵 C − A C-A CA中对应的行均为0,表示进程已终止。
  4. 重复步骤2和3,直到所有的进程都标记为终止,则状态是安全的

4. 哲学家就餐问题

① 问题描述
  • 有5个哲学家,他们围坐在一张餐桌旁,餐桌上有5盘意大利面、5把叉子。
  • 每个哲学家的动作只有思考和吃饭,并且吃饭时需要使用两只叉子。
    操作系统之死锁(死锁原因、四个必要条件、如何解锁死锁、银行家算法、哲学家就餐问题)_第8张图片
  • 如果每位哲学家都先拿起左手边的叉子,再拿起右手手边的叉子。吃完面后,这两把叉子又被放到桌子上。这会导致死锁:如果所有的哲学家都同一时间感到饥饿,他们同时先拿起左手边的叉子,然后伸手拿右手边的叉子,而右手边的叉子已经被拿,所有的哲学家都会处于饥饿状态。
  • 哲学家就餐问题,实现的算法必须保持互斥(不能有两个哲学家拿到同一把叉子),还要避免死锁(所有的哲学家都吃不上)和饥饿(某些哲学家一直吃不上)。
② 哲学家就餐问题的解决方法
  • 添加一个服务生,只有经过服务生允许后哲学家才能拿叉子,由服务生负责避免死锁
  • 哲学家必须确定左右两边的叉子都可用后,才能同时拿起左右两边的叉子
  • 给叉子编号,每位哲学家每次只能拿起编号较小的叉子,最后一位哲学家无法找到编号小的叉子,则不能拿起叉子。多余的叉子将由其他哲学家拿起。这种方式虽然避免了死锁,但是资源利用率不高。
③ 同时拿起的叉子的Java实现
  • 设计思想:
  1. 一个哲学家是一个线程,它不停地进行思考、吃饭。
  2. 为了模拟思考和吃饭的过程,该线程需要休眠一定时间。
  3. 共有5把叉子,每把叉子的状态为可用或不可用。
  4. 吃饭时,要求哲学家左右两边的叉子都是可用的,才能同时拿起叉子(将叉子的状态设置为不可用)。
  5. 哲学家放下叉子时,需要将叉子状态设置为可用,并通知在等待的其他哲学家。
  • Java实现:
public class PhilosopherProblem {
    public static void main(String[] arg){
        Fork fork=new Fork();
        Philosopher philosopher0=new Philosopher("0",fork);
        Philosopher philosopher1=new Philosopher("1",fork);
        Philosopher philosopher2=new Philosopher("2",fork);
        Philosopher philosopher3=new Philosopher("3",fork);
        Philosopher philosopher4=new Philosopher("4",fork);
        philosopher0.start();
        philosopher1.start();
        philosopher2.start();
        philosopher3.start();
        philosopher4.start();
    }
}
class Philosopher extends Thread {
    private String name;
    private Fork fork;
    public Philosopher(String name, Fork fork) {
        super(name);// 更新自身线程名,同时需要设置自己的name
        this.name=name;
        this.fork = fork;
    }
    @Override
    public void run() {
        while (true) {
            think();
            fork.takeFork();
            eat();
            fork.putFork();
        }
    }
    public void think() {
        System.out.println("Philosopher" + name + ": I'm thinking...");
        try {
            Thread.sleep(1000);// 模拟思考
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void eat() {
        System.out.println("Philosopher" + name + ": I'm eating...");
        try {
            Thread.sleep(1000);// 模拟吃饭
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
class Fork {
    // 初始时,五支叉子都未被使用
    private boolean[] fork = new boolean[5];
    // 只有当左右两边的叉子都同时可用时,才能拿起叉子
    public synchronized void takeFork() {
        String name = Thread.currentThread().getName();
        int i = Integer.valueOf(name);
        while (fork[i] || fork[(i + 1) % 5]) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 同时拿起叉子,更新叉子状态
        fork[i] = true;
        fork[(i + 1) % 5] = true;
    }
    // 同时释放左右手的叉子并通知等待的哲学家
    public synchronized void putFork() {
        String name = Thread.currentThread().getName();
        int i = Integer.valueOf(name);
        // 更新叉子的状态
        fork[i] = false;
        fork[(i + 1) % 5] = false;
        notifyAll(); // 通知在等待的哲学家
    }
}

操作系统之死锁(死锁原因、四个必要条件、如何解锁死锁、银行家算法、哲学家就餐问题)_第9张图片

5. 死锁的问题总结

1. 使用java代码实现两个线程死锁。

  • 两个线程分别访问两种共享数据,加锁顺序不一致导致死锁。

2. 什么是死锁?死锁的必要条件?

  • 必要条件:互斥、占有且等待、不可抢占、循环等待

3. 如何解决死锁?

  • 鸵鸟策略
  • 死锁的预防: 破坏死锁的四个必要条件,互斥(无法禁止)、占有且等待(一次性请求所有需要的资源,低效、不太现实)、不可抢占(被拒绝就释放已经占有资源或者允许高优级先抢占低优先级的资源)、循环等待(资源编号,顺序获取)
  • 死锁的避免: 采用一些算法避免系统进入不安全状态,两种策略:进程启动拒绝(总是假设最坏情况)、资源分配拒绝(银行家算法)
  • 死锁的检测与恢复: 检测死锁的发生并进行恢复,恢复:取消所有死锁的进程、回滚、连续释放资源、连续取消进程。

4. java如何避免死锁?

  • 按顺序加锁: 哲学家就餐问题中,要求同时获取到共享数据。
  • 超时放弃锁: 通过带时间参数的tryLock()方法,如果超时获取锁失败,则释放已经获取到的锁。

5. MySQL中的死锁

  • 资金A先后分配给借款人1、2,资金B先后分配给借款人2、1。
  • 解决办法:一次性锁住所有能分配到资金的借款人。

6. 死锁、活锁、饥饿的区别?

  • 死锁:
  1. 概念: 两个或两个以上的进程,由于资源竞争或由于彼此通信而造成的一种阻塞现象。若无外力作用,他们都将无法推进下去。
  2. 形象的解释: 单向通行的木桥,A和B同时从桥的两端上桥。到了桥上相遇后,却都不愿意退回桥头,导致两个人都在桥上,无法继续前进。
  • 活锁:
  1. 概念: 任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试——失败——尝试——失败的过程。
  2. 活锁多发生于优先级相同的进程之间: 进程A明明可以使用资源,他却很绅士让其他进程先使用资源;进程B也明明可以使用资源,却很绅士的让其他进程先使用。这样多个进程你让我,我让你,最后谁都无法使用资源。
  3. 形象的解释: 单向通行的木桥,A和B同时走到桥头。A让B先走,B又让A先走,大家相互谦让,结果谁都没有过桥。
  • 饥饿
  1. 概念: 一个进程虽然能执行,却被调度器无限期地忽视,而不能被调度执行的情况。
  2. 饥饿发生的场景: 非抢占式的短作业优先调度算法,可能会导致长作业饥饿;优先级调度算法,也可能会导致低优先级的进程饥饿。Java中的ReentrantLock非公平访问,可能会造成线程饥饿。
  3. 形象的解释: 哥哥照顾弟弟妹妹,有食物总是让弟弟妹妹先吃。而邻居家的孩子不断地来,导致哥哥最后没吃上东西,饿得不行!

你可能感兴趣的:(操作系统)