欢迎光临散文网 会员登陆 & 注册

阿里技术三面:哲学家进餐

2022-05-08 11:16 作者:做架构师不做框架师  | 我要投稿

哲学家进餐

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

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

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

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

哲学家从 0 到 4 按顺时针编号。

请实现函数 wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork):

  • philosopher 哲学家的编号。

  • pickLeftFork 和 pickRightFork 表示拿起左边或右边的叉子。

  • eat 表示吃面。

  • putLeftFork 和 putRightFork 表示放下左边或右边的叉子。

  • 由于哲学家不是在吃面就是在想着啥时候吃面,所以思考这个方法没有对应的回调。

给你 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 号哲学家拿起了右边的叉子。

解题思路

这道题本质上其实是想考察如何避免死锁。

易知:当 5个哲学家都拿着其左边(或右边)的叉子时,会进入死锁。

死锁的4个必要条件

  • 互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

  • 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

  • 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

  • 循环等待条件:若干进程间形成首尾相接循环等待资源的关系。

4个哲学家持有叉子?

最多只允许 4 个哲学家去持有叉子,可保证至少有 1个哲学家能吃上意大利面(即获得到 2 个叉子)。

因为最差情况下是:4 个哲学家都各自持有1个叉子,此时还剩余 1个叉子可供使用,这 4个哲学家中必然有1人能获取到这个剩余的 1 个叉子,从而手持 2个叉子,可以吃意大利面。

即:4 个人中,1 个人有 2 个叉子,3 个人各持 1 个叉子,共计 5 个叉子。

3个哲学家持有叉子?

既然最多只允许4个哲学家去持有叉子,那么如果只允许 3 个哲学家去持有叉子是否可行呢?

当然可行,3个哲学家可以先都各自持有 1 把叉子,此时还剩余 2 把叉子;

当这 3 个哲学家刚好都相邻(比如:编号为图中的 0, 1, 2 ),可能会造成只有 1 个哲学家能吃到意面的情况,具体而言即 0号哲学家拿到了其左侧的叉子(编号为 1 ), 1号哲学家也拿到了其左侧的叉子(编号为 2 ),2 号哲学家也拿到了其左侧的叉子(编号为 3 ),此时只有 0号哲学家能拿到其右侧的叉子(编号为 0 ),因此只有 0 号哲学家能吃到意面。

而其余情况下,3个哲学家中都能有 2人吃到意面。

即:3 个人中,2 个人各自持有 2 个叉子,1 个人持有 1 个叉子,共计 5 个叉子。

并且仔细想想,叉子的数目是固定的(个数为 5 ),直觉上来讲 3 个人去抢 5 个叉子比 4 个人去抢 5 个叉子效率高

方法一:信号量

用Semaphore去实现上述的限制:Semaphore eatLimit = new Semaphore(4);

一共有5个叉子,视为5个ReentrantLock,并将它们全放入1个数组中。

给叉子编号 0, 1, 2, 3, 40,1,2,3,4(对应数组下标)。

代码具体实现:

改进:

位运算就可以表示5个叉子的使用状态,只需用1个volatile修饰的int变量即可 + CAS操作即可,即AtomicInteger类。

代码具体实现:

方法二:只允许1个哲学家就餐

设置 1 个临界区以实现 1 个哲学家 “同时”拿起左右 2 把叉子的效果。

即进入临界区之后,保证成功获取到左右 2 把叉子并执行相关代码后,才退出临界区。

方法2有一定的概率是“并行”,“只允许1个哲学家就餐”是严格的“串行”。

代码如下:

改进:

位运算就可以表示5个叉子的使用状态,只需用1个volatile修饰的int变量即可 + CAS操作即可,即AtomicInteger类。

方法一和方法二的区别

2 者之间有细微的差别

方法 2 是在成功拿起左右叉子之后就退出临界区,而“只让 1 个哲学家就餐”是在拿起左右叉子 + 吃意面 + 放下左右叉子一套流程走完之后才退出临界区。

前者的情况可大概分为 2 种,举具体例子说明(可参照上面给出的图片):

  • 1 号哲学家拿起左右叉子( 1 号叉子 + 2 号叉子)后就退出临界区,此时 4 号哲学家成功挤进临界区,他也成功拿起了左右叉子( 0 号叉子和 4 号叉子),然后就退出临界区。

  • 1 号哲学家拿起左右叉子( 1 号叉子 + 2 号叉子)后就退出临界区,此时 2 号哲学家成功挤进临界区,他需要拿起 2 号叉子和 3 号叉子,但 2 号叉子有一定的概率还被 1 号哲学家持有( 1 号哲学家意面还没吃完),因此 2 号哲学家进入临界区后还需要等待 2 号叉子。至于 3 号叉子,根本没其他人跟 2 号哲学家争夺,因此可以将该种情况视为“ 2 号哲学家只拿起了 1 只叉子,在等待另 1 只叉子”的情况。

总之,第1种情况即先后进入临界区的 2 位哲学家的左右叉子不存在竞争情况,因此先后进入临界区的 2 位哲学家进入临界区后都不用等待叉子,直接就餐。此时可视为 2 个哲学家在同时就餐(当然前 1 个哲学家有可能已经吃完了,但姑且当作是 2 个人同时就餐)。

第 2 种情况即先后进入临界区的 2 位哲学家的左右叉子存在竞争情况(说明这 2 位哲学家的编号相邻),因此后进入临界区的哲学家还需要等待 1 只叉子,才能就餐。此时可视为只有 1 个哲学家在就餐。

至于“只允许 1 个哲学家就餐”的代码,很好理解,每次严格地只让 1 个哲学家就餐,由于过于严格,以至于都不需要将叉子视为ReentrantLock。

方法三

前面说过,该题的本质是考察如何避免死锁。

而当 5 个哲学家都左手持有其左边的叉子或当 5 个哲学家都右手持有其右边的叉子时,会发生死锁。

故只需设计1个避免发生上述情况发生的策略即可。

即可以让一部分哲学家优先去获取其左边的叉子,再去获取其右边的叉子;再让剩余哲学家优先去获取其右边的叉子,再去获取其左边的叉子。

代码如下:

改进:

位运算就可以表示5个叉子的使用状态,只需用1个volatile修饰的int变量即可 + CAS操作即可,即AtomicInteger类。

写在最后

本文内容出处是力扣官网,希望和大家一起刷算法,在后面的路上不变秃但是变强!

好兄弟可以点赞并关注我的公众号“javaAnswer”,全部都是干货。


阿里技术三面:哲学家进餐的评论 (共 条)

分享到微博请遵守国家法律