标签:png 过程 ini lock list 概率 资源 row ant
Q:5 个沉默寡言的哲学家围坐在圆桌前,每人面前一盘意面。叉子放在哲学家之间的桌面上。(5 个哲学家,5 根叉子)
所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的叉子才能吃到面,而同一根叉子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把叉子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的叉子,但在没有同时拿到左右叉子时不能进食。
假设面的数量没有限制,哲学家也能随便吃,不需要考虑吃不吃得下。
设计一个进餐规则(并行算法)使得每个哲学家都不会挨饿;也就是说,在没有人知道别人什么时候想吃东西或思考的情况下,每个哲学家都可以在吃饭和思考之间一直交替下去。
哲学家从?0 到 4 按 顺时针 编号。请实现函数?void 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:
引用:@?.?G?F?u?'? 、? ?|
这个题目是防止死锁,每个哲学家都拿起左手或右手,导致死锁
1.第一种方法是设置一个信号量,当前哲学家会同时拿起左手和右手的叉子直至吃完。即有3 个人中,2 个人各自持有 2 个叉子,1 个人持有 1 个叉子,共计 5 个叉子。
用Semaphore去实现上述的限制:Semaphore eatLimit = new Semaphore(4);
一共有5个叉子,视为5个ReentrantLock,并将它们全放入1个数组中。
设置编码:
代码:
class DiningPhilosophers {
//1个Fork视为1个ReentrantLock,5个叉子即5个ReentrantLock,将其都放入数组中
private ReentrantLock[] locks = {new ReentrantLock(), new ReentrantLock(), new ReentrantLock(), new ReentrantLock(), new ReentrantLock()};
//限制 最多只有4个哲学家去持有叉子
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();//限制人数减一
locks[leftFork].lock();
locks[rightFork].lock();
pickLeftFork.run();
pickRightFork.run();
eat.run();
putLeftFork.run();
putRightFork.run();
locks[leftFork].unlock();
locks[rightFork].unlock();
eatLimit.release();
}
}
2.设置 1 个临界区以实现 1 个哲学家 “同时”拿起左右 2 把叉子的效果。即进入临界区之后,保证成功获取到左右 2 把叉子 并 执行相关代码后,才退出临界区。
与上一种的差别是“允许1个哲学家用餐”。方法2是在成功拿起左右叉子之后就退出临界区,而“只让1个哲学家就餐”是在拿起左右叉子 + 吃意面 + 放下左右叉子 一套流程走完之后才退出临界区。
前者的情况可大概分为2种,举具体例子说明(可参照上面给出的图片):
总之,第1种情况即先后进入临界区的2位哲学家的左右叉子不存在竞争情况,因此先后进入临界区的2位哲学家进入临界区后都不用等待叉子,直接就餐。此时可视为2个哲学家在同时就餐(当然前1个哲学家有可能已经吃完了,但姑且当作是2个人同时就餐)。
第2种情况即先后进入临界区的2位哲学家的左右叉子存在竞争情况(说明这2位哲学家的编号相邻),因此后进入临界区的哲学家还需要等待1只叉子,才能就餐。此时可视为只有1个哲学家在就餐。
至于“只允许1个哲学家就餐”的代码,很好理解,每次严格地只让1个哲学家就餐,由于过于严格,以至于都不需要将叉子视为ReentrantLock。
方法2有一定的概率是“并行”,“只允许1个哲学家就餐”是严格的“串行”。
代码:
class DiningPhilosophers {
//1个Fork视为1个ReentrantLock,5个叉子即5个ReentrantLock,将其都放入数组中
private ReentrantLock[] lockList = {new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock()};
//让 1个哲学家可以 “同时”拿起2个叉子(搞个临界区)
private ReentrantLock pickBothForks = new ReentrantLock();
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; //右边的叉子 的编号
pickBothForks.lock(); //进入临界区
lockList[leftFork].lock(); //拿起左边的叉子
lockList[rightFork].lock(); //拿起右边的叉子
pickLeftFork.run(); //拿起左边的叉子 的具体执行
pickRightFork.run(); //拿起右边的叉子 的具体执行
pickBothForks.unlock(); //退出临界区
eat.run(); //吃意大利面 的具体执行
putLeftFork.run(); //放下左边的叉子 的具体执行
putRightFork.run(); //放下右边的叉子 的具体执行
lockList[leftFork].unlock(); //放下左边的叉子
lockList[rightFork].unlock(); //放下右边的叉子
}
}
3.前面说过,该题的本质是考察 如何避免死锁。
而当5个哲学家都左手持有其左边的叉子 或 当5个哲学家都右手持有其右边的叉子时,会发生死锁。
故只需设计1个避免发生上述情况发生的策略即可。
即可以让一部分哲学家优先去获取其左边的叉子,再去获取其右边的叉子;再让剩余哲学家优先去获取其右边的叉子,再去获取其左边的叉子。
代码:
class DiningPhilosophers {
//1个Fork视为1个ReentrantLock,5个叉子即5个ReentrantLock,将其都放入数组中
private ReentrantLock[] lockList = {new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock(),
new ReentrantLock()};
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; //右边的叉子 的编号
//编号为偶数的哲学家,优先拿起左边的叉子,再拿起右边的叉子
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(); //放下右边的叉子
}
}
改进:改进代码看3种解法(互斥锁或volatile)
1.ReentrantLock和synchronize关键字都是使用互斥量的重量级锁,而volatile关键字相较于它们就比较“轻量”。
因此把ReentrantLock数组改为使用volatile修饰的boolean数组。
PS: volatile要和原子操作搭配使用才能保证同步。
而对volatile变量赋 常量值 可看为是原子操作。
看着后面这种解法更清晰:
每个人都可以尝试去吃东西,吃东西前尝试去拿左边的叉子和右边的叉子,这样就可以想到使用信号量Semaphore的tryAcquire方法。
这里竞争的资源是叉子,所以定义代表5个叉子的信号量即可。
代码:
class DiningPhilosophers {
int num = 5;
//五个叉子的信号量
private Semaphore[] semaphores = new Semaphore[5];
public DiningPhilosophers() {
for (int i = 0; i < num; i++) {
//每只叉子只有1个
semaphores[i] = new Semaphore(1);
}
}
// 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 left = philosopher;
//右边叉子的位置
int right = (philosopher + 1) % num;
while (true) {
if (semaphores[left].tryAcquire()) {
//先尝试获取左边叉子,如果成功再尝试获取右边叉子
if (semaphores[right].tryAcquire()) {
//两个叉子都得到了,进餐
pickLeftFork.run();
pickRightFork.run();
eat.run();
putLeftFork.run();
//释放左边叉子
semaphores[left].release();
putRightFork.run();
//释放右边边叉子
semaphores[right].release();
//吃完了,就跳出循环
break;
} else {
//如果拿到了左边的叉子,但没拿到右边的叉子: 就释放左边叉子
semaphores[left].release();
//让出cpu等一会
Thread.yield();
}
} else {
//连左边叉子都没拿到,就让出cpu等会吧
Thread.yield();
}
}
}
}
标签:png 过程 ini lock list 概率 资源 row ant
原文地址:https://www.cnblogs.com/xym4869/p/12742950.html