哲学家用餐问题

在这里插入图片描述

1. 条件语句的原子性

你可能会有点疑惑,这不是讲哲学家用餐问题的吗,为什么一上来你先甩一张毫无相干的图,这里又提条件语句的原子性?
别着急,一会儿你就会看到原因了。
我们先来看看上面的图,右上角是java语言写的语句;左下角是经javap得到的汇编代码。使用箭头将对应部分连接了起来。
可以看到一个最简单的if条件语句(图中蓝色框)都至少要两条指令,一条用于取操作数(在i > 0这个比较中0是个常量,不需要取操作),一条用于比较转移(即如果不成功跳转到哪一步执行,因为如果成功直接顺序执行即可)。如果复杂一点的(如图中红色框),需要的指令更多了(当然了由于&&|| 具有短路性质,因此指令多少不是只看条件句中&&|| 的个数)。

总之 条件语句在java中不是原子操作。其实还可以看到图中的i = j + 1;需要四条指令:1. 取j; 2. 取常量1; 3. add操作; 4. 保存结果到i。因此这个也不是原子操作。再说一下,对于return操作,也不是原子操作,就算最简单的return 0;都是这样的:
在这里插入图片描述
因此,我想说的是,如果遇到多线程问题,该加锁的一定要加锁,该互斥的一定要互斥。

好了,接下来言归正传:

2. 哲学家就餐问题

由Dijkstra提出并解决的哲学家就餐问题是典型的同步问题。该问题描述的是五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个盘子和五只叉子,他们的生活方式是交替的进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左手边和右手边的叉子,只有在他能够拿到左右手的两只叉子时才能进餐。进餐完毕,放下筷子继续思考。不能让哲学家饿死,即不能发生线程的死锁。
死锁产生的条件,只有当以下四个条件同时满足时,才有可能会发生死锁。这四个条件是必要不充分条件:
在这里插入图片描述

  1. 互斥条件。也就是说多个线程所使用的资源中至少有一个是不能共享的。这里面就是叉子,一个哲学家拿起了叉子,他的邻居就不能再使用了;
  2. 至少有一个任务他必须持有一个资源且正在等待获取一个当前被其他任务所持有的资源。也就是说,要发生死锁,哲学家必须拿着一只叉子并且等待另一只;
  3. 资源不能被任务抢占。也就是说哲学家必须很有礼貌的等待别人自己放下叉子,而不是去抢叉子;
  4. 循环等待。一个任务等待其他任务的资源,后者又在等待另一个任务的资源,这样一直下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都得不到全部资源运行而被锁住。也就是0号哲学家等待1号的叉子,1号等待2号的,…,最终4号等待0号的,于是大家都陷入死等状态。

要发生死锁的话,上面四个条件必须同时满足,因此只要破除其中任何一个,就能避免死锁的发生。

3. 解决方法

在这里插入图片描述

  1. 思路: 伪代码见上图。每个人都先去拿自己左手边的叉子,然后再去拿自己右手边的叉子,如果能拿到两把叉子就可以吃饭,吃完饭后先放下左手边的叉子,然后放下右手边的叉子;否则就一直等待直到能拿起右手的叉子。
    分析: 假如每个人都拿到了左手边的叉子,然后再尝试拿右手边的时候,就会因为得不到右手的叉子而陷入死等状态。由于每个人都不想让,因此大家都只能饿死。
    在这里插入图片描述

  2. 思路: 上面可以发现,陷入死锁是因为每个人在拿到左手边的叉子后不放手,导致每个人都得不到右手边的叉子。我们进行一下改进,那就是如果得不到右手边的叉子,就把左手的放下后再进入等待状态,等一等然后再重复上述操作直到能拿起两个叉子。
    分析: 毕竟是哲学家,受过教育的人高素质人才,怎么能不懂得谦让呢。但是如果每个人都拿起了左手的叉子,然后发现拿不到右手的叉子,又都放下了左手的叉子,然后等了一会儿,又都开始拿起左手的叉子然后去尝试拿右手的,又拿不到了,继续等,然后。。。这样下去大家又都要饿死了。看来过分谦让也不是一件好事。这种事情其实生活中经常碰到,一个小路你和一个人迎面走来,眼看要碰到,于是你给他让路他也给你让路,然后你俩尴尬的一笑,你又重新走回原来那边,他也是,于是你俩就这么一直互相让着让着终于碰到了一起。

    在这里插入图片描述

  3. 思路: 我们发现哲学家的动作太整齐划一了,左手画彩虹,右手一条龙。为了使他们动作不再那么整齐,当放下左手的叉子的时候,每个人等待的时间随机,然后再重复操作。
    分析: 这样的话,动作快的就能先吃到饭把叉子放下,然后动作慢的最终也能吃到饭。但是这个等待的时间是随机的,属于不可控因素。并且吃饭的效率也有问题,因为有5个叉子,所以最多可以同时满足两个人(任意不相邻的两个)吃饭,例如0号哲学家和2号哲学家同时吃饭是可以保证的。

  4. 思路: 经过上面的分析我们发现,饿死的情形中都是由于他们都是先拿的左手叉子后拿的右手叉子,因此发生了死锁。我们换一种方式,规定偶数号哲学家(即0号、2号、4号)先拿左手边叉子,而奇数号哲学家(1号和3号)先拿右手边叉子。然后再尝试拿另一只叉子,如果能拿到就吃饭吃完放下双手的叉子,如果拿不到就放下手中的叉子,等待一会儿再重新去拿叉子。。。
    在这里插入图片描述
    分析: 如上图所示,0-4五个哲学家围在一个桌子周围,偶数号哲学家先拿左手边(红色箭头)的叉子,奇数号哲学家先拿右手边(蓝色箭头)的叉子。这时4号哲学家一定可以得到两个叉子,从而先吃到饭后将叉子放下,其他的哲学家也能够吃到饭。
    首先0号拿到一个叉子,1号没拿到就等待,2号拿到,则3号此时拿不到叉子等待,4号拿到叉子;然后0号拿另一个发现被4号占了于是放下叉子等待,1号在等待中,2号顺利拿到另一个叉子,3号拿不到叉子继续等待,4号可以拿到另一个叉子。这时候同时有2号和4号都能够吃饭。当他俩吃完放下叉子,3号一定可以拿到两个叉子(因为2号和4号都放下了叉子),剩下的0号和1号也一定有一个可以拿到两个叉子(假设是1号哲学家),这时又可以同时有两个哲学家能够吃饭,吃完饭放下叉子,还是可以满足两个同时吃饭的条件(2号一定可以拿到两个叉子,0号和4号其中有一个可以拿到两把叉子)。。。
    这个方案是好的,不仅不会导致有人饿死,而且还提高了吃饭的效率,同时可以有两个人吃饭。
    它是通过破除循环等待条件,避免死锁的。

  5. 思路: 另一种思路是限制拿叉子的人数。观察上面的死锁是因为五个人同时拥有了一支叉子导致每个人都得不到另一个。假如我们规定每次最多只有四个哲学家能同时拿起左手的叉子,然后再去拿右手的叉子的时候,必然有一个人能够得到右手边的叉子从而吃完饭。
    在这里插入图片描述
    分析: 假如0、1、2、3都先拿到了一个左叉子,这时候4就只能等待了;然后0、1、2、3再去拿右叉子的时候因为4没有拿左叉子,所以0可以拿到右叉子,吃完饭放下叉子。然后1可以吃饭,接着是2,2吃的时候0也可以吃,2吃完3吃,3吃完4吃。
    可以看到这个吃饭的效率降低了一些,并且吃饭的顺序有了一定的约束。根据这个思路,我们还可以想到另一个方法。

  6. 思路: 规定哲学家的吃饭顺序,按照他们的序号从小到大。
    分析: 这样效率更低了,退化成了完全的顺序执行方式。

  7. 思路: 哲学家在拿叉子的时候不再每次只拿一个了,而是一次拿两个能成功就吃饭,不成功就等待。如果哲学家的左邻和右邻都没有在用餐,则他可以同时拿起两个叉子吃饭,否则他就等待。
    分析: 因为死锁的条件是每一个哲学家都正好拿到了一只叉子等待另一只。但是这里哲学家要么拿到的是两个叉子要么是一个叉子都拿不到,不会出现死锁的情况。
    实现: 由于要实现同时去拿两把叉子的操作,因此需要一个互斥的信号量mutex保证这个过程的原子性。第一节条件语句的原子性就是为了这里而写的。如下:

    //伪代码
    P(mutex);
    //如果不是在临界区里面,可能会发生这种情况:当我观察到左邻居没有在吃饭后,
    //我的执行权限给了左邻居,然后他发现他的左邻居没吃饭而我也还没有开始吃饭,
    //他就去吃饭了,这时执行权重新给了我,然后我继续观察发现右邻居没有吃饭,
    //于是我也去吃饭了。诡异的事情来了,我和我的邻居居然在同时吃饭。
    //显然这是不允许的。问题就出现
  • 6
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值