2.20、哲学家进餐问题

一张圆桌上坐着 5 名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。

哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响他人。

只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。

饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。

image-20230204003023792

1、问题分析

  1. 关系分析。系统中有5个哲学家进程,5位哲学家与左右邻居对其中间筷子的访问是互斥关系。

  2. 整理思路。这个问题中只有互斥关系,但与之前遇到的问题不同的事,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。如何避免临界资源分配不当造成的死锁现象,是哲学家问题的精髓。

  3. 信号量设置。定义互斥信号量数组 chopstick[5]={1,1,1,1,1} 用于实现对 5 个筷子的互斥访问。

    并对哲学家按 0~4 编号,哲学家i左边的筷子编号为 i ,右边的筷子编号为 (i+1)%5

错误

image-20230204003436560

循环的等待


2、如何避免死锁的发生呢?(附 Java 实现)

① 可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。(破坏循环等待条件,本来 5 个哲学家对应 5 个筷子,现在 4 个哲学家对应 5 个筷子)

这样可以保证至少有一个哲学家是可以拿到左右两只筷子的

image-20230204003718420

semaphore chopstick [5] = {1,1,1,1,1};
semaphore mutex = 4; //只允许 4 个哲学家进行争抢
pi () {
    while (1) {
        P(mutex);
        P(chopstick[i]);			//拿左
        P(chopstick[(i + 1) % 5]);  //拿右
        V(mutex);					//V 顺序无所谓
        吃饭...
        V(chopstick[i]);			//放左
        V(chopstick[(i + 1) % 5]);  //放右
        思考...
    }
}

Java 实现

/**
 * 最多允许四个哲学家同时进餐
 */
class DiningPhilosophers {

    public DiningPhilosophers() {

    }

    Lock[] chopstick = new ReentrantLock[]{
            new ReentrantLock(),
            new ReentrantLock(),
            new ReentrantLock(),
            new ReentrantLock(),
            new ReentrantLock()
    };
    Semaphore mutex = new Semaphore(4);

    // 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 {
        //信号量 -1
        mutex.acquire();
        //拿起左右筷子
        chopstick[philosopher].lock();
        chopstick[(philosopher + 1) % 5].lock();
        pickLeftFork.run();
        pickRightFork.run();
        //eat
        eat.run();
        //放下左右筷子
        putLeftFork.run();
        putRightFork.run();
        chopstick[philosopher].unlock();
        chopstick[(philosopher + 1) % 5].unlock();
        //信号量 + 1
        mutex.release();
    }
}

② 要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。(破坏循环等待,分为奇偶数,给上编号)

用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一支后再等待另一只的情况。

semaphore chopstick [5] = {1,1,1,1,1};
semaphore mutex = 1; //互斥地取筷子
pi () {
    while (1) {
        P(mutex);
        //偶数
        if (i & 1 == 0) {
	        P(chopstick[(i + 1) % 5]);  //拿右   
	        P(chopstick[i]);			//拿左
        }else {
            P(chopstick[i]);			//拿左
	        P(chopstick[(i + 1) % 5]);  //拿右   
        }
        V(mutex);					//V 顺序无所谓
        吃饭...
        V(chopstick[i]);			//放左
        V(chopstick[(i + 1) % 5]);  //放右
        思考...
    }
}

image-20230204004009243

Java 实现

/**
 * 奇数号哲学家先左后右, 偶数号哲学家先右后左
 */
class DiningPhilosophers {

    public DiningPhilosophers() {

    }

    Lock[] chopstick = new ReentrantLock[]{
            new ReentrantLock(),
            new ReentrantLock(),
            new ReentrantLock(),
            new ReentrantLock(),
            new ReentrantLock()
    };

    // 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 {
        //尝试拿起左右筷子
        if ((philosopher & 1) != 0) {
            chopstick[philosopher].lock();
            chopstick[(philosopher + 1) % 5].lock();
        }else {
            chopstick[(philosopher + 1) % 5].lock();
            chopstick[philosopher].lock();
        }

        pickLeftFork.run();
        pickRightFork.run();
        //eat
        eat.run();
        putLeftFork.run();
        putRightFork.run();
        //放下左右筷子
        chopstick[philosopher].unlock();
        chopstick[(philosopher + 1) % 5].unlock();
    }
}

③ 仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。(破坏请求和保持条件,预先分配 2 个筷子)

如下所示:

  1. 若 0 号哲学家正在 P(chopstick[i]) 中,此时 2 号哲学家也想拿筷子,将会阻塞在 P(mutex) ,直到等待 0 号哲学家 V(mutex)

  2. 若 0 号哲学家正在吃饭,0, 1 号筷子被使用;此时 1 号哲学家将会阻塞 P(chopstick[i]);(0 号筷子), mutex = 0; 若 2 号哲学家也想拿筷子,则会被阻塞到 P(mutex)

    • 即使 2 号左右两边的筷子都在,也暂时无法取得
  3. 若 0 号哲学家正在吃饭,0, 1 号筷子被使用;此时 4 号哲学家将拿起左边筷子,但是阻塞在 P(chopstick[(i + 1) % 5]) ,直到等待 0 号哲学家吃完饭 V(chopstick[i]);

    • 此时 4 号右边的筷子不可用,但 4 号仍然会拿起左边的筷子
semaphore chopstick [5] = {1,1,1,1,1};
semaphore mutex =1; //互斥地取筷子
Pi () {				//i 号哲学家的进程
	while (1) {
        P(mutex);
        P(chopstick[i]);			//拿左
        P(chopstick[(i + 1) % 5]);  //拿右
        V(mutex);
        吃饭...
        V(chopstick[i]);			//放左
        V(chopstick[(i + 1) % 5]);  //放右
        思考...
    }	
}

因此上述方法并不能保证只有两边的筷子都可用时,才允许哲学家拿起筷子

  • 例如上述的情况 1 和 2 都会导致 2 号哲学家两边的筷子都可以用,但是却被阻塞

更准确的说法应该是

  • 各哲学家拿筷子这件事必须互斥的执行。

    这就保证了即使一个哲学家在拿筷子拿到一半时被阻塞,也不会有别的哲学家会继续尝试拿筷子。

    这样的话,当前正在吃饭的哲学家放下筷子后,被阻塞的哲学家就可以获得等待的筷子了。

Java 实现

class DiningPhilosophers {

    public DiningPhilosophers() {

    }

    Lock[] chopstick = new ReentrantLock[]{
            new ReentrantLock(),
            new ReentrantLock(),
            new ReentrantLock(),
            new ReentrantLock(),
            new ReentrantLock()
    };
    Lock mutex = new ReentrantLock();

    // 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 {

        //由于要同时持有,所以要互斥
        mutex.lock();
        chopstick[philosopher].lock();
        chopstick[(philosopher + 1) % 5].lock();
        mutex.unlock();

        pickLeftFork.run();
        pickRightFork.run();
        //eat
        eat.run();
        putLeftFork.run();
        putRightFork.run();
        //放下左右筷子
        chopstick[philosopher].unlock();
        chopstick[(philosopher + 1) % 5].unlock();
    }
}

3、位运算 + CAS 优化

方式① 最多允许四个哲学家同时进餐

/**
 * 最多允许四个哲学家同时进餐
 */
class DiningPhilosophers {

    public DiningPhilosophers() {

    }

    //若 chopstick 等于 0, 表示筷子未被使用
    AtomicInteger chopstick = new AtomicInteger();
    // 00001, 00010, 00100, 01000, 10000
    int[] chopstickMask = new int[]{1, 2, 4, 8, 16};

    Semaphore mutex = new Semaphore(4);

    // 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 leftC = chopstickMask[philosopher];
        int rightC = chopstickMask[(philosopher + 1) % 5];
        //信号量 -1
        mutex.acquire();
        //尝试拿起左右筷子
        //CAS
        while (!get(leftC)) {
            Thread.sleep(1);
        }
        while (!get(rightC)) {
            Thread.sleep(1);
        }
        pickLeftFork.run();
        pickRightFork.run();
        //eat
        eat.run();
        putLeftFork.run();
        putRightFork.run();
        //放下左右筷子
        while (!put(leftC)) {
            Thread.sleep(1);
        }
        while (!put(rightC)) {
            Thread.sleep(1);
        }

        //信号量 + 1
        mutex.release();
    }

    private boolean put(int mask) {
        int expect = chopstick.get();
        //例如:1 号放下筷子 00001 和 00010
        // 左边的筷子 expect : 00011 ^ 00001 -> 00010
        // 右边的筷子 expect : 00010 ^ 00010 -> 00000
        return chopstick.compareAndSet(expect, expect ^ mask);
    }

    //尝试拿起左右筷子
    private boolean get(int mask) {
        int expect = chopstick.get();
        //只有当前筷子没有被拿起才返回 true, 并且更新筷子状态
        // 例如:1 号拿起筷子 00001 和 00010 , 更新后 : 00011 (说明1, 2号筷子被拿了)
        //  此时 2 号想拿起左边的筷子 : 00010 , & 运算发现被拿了, 返回false
        return (expect & mask) <= 0 && chopstick.compareAndSet(expect, expect ^ mask);
    }
}

方式② 奇数号哲学家先左后右, 偶数号哲学家先右后左

import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 奇数号哲学家先左后右, 偶数号哲学家先右后左
 */

class DiningPhilosophers {

    public DiningPhilosophers() {

    }

    //若 chopstick 等于 0, 表示筷子未被使用
    AtomicInteger chopstick = new AtomicInteger();
    // 00001, 00010, 00100, 01000, 10000
    int[] chopstickMask = new int[]{1, 2, 4, 8, 16};


    // 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 leftC = chopstickMask[philosopher];
        int rightC = chopstickMask[(philosopher + 1) % 5];
        //信号量 -1
        if ((philosopher & 1) != 0) {
            //尝试拿起左右筷子
            while (!get(leftC)) {
                Thread.sleep(1);
            }
            while (!get(rightC)) {
                Thread.sleep(1);
            }
        } else {
            //尝试拿起右左筷子
            while (!get(rightC)) {
                Thread.sleep(1);
            }
            while (!get(leftC)) {
                Thread.sleep(1);
            }

        }

        //退出临界区

        pickLeftFork.run();
        pickRightFork.run();
        //eat
        eat.run();
        putLeftFork.run();
        putRightFork.run();
        //放下左右筷子
        while (!put(leftC)) {
            Thread.sleep(1);
        }
        while (!put(rightC)) {
            Thread.sleep(1);
        }
        //信号量 + 1
    }

    private boolean put(int mask) {
        int expect = chopstick.get();
        //例如:1 号放下筷子 00001 和 00010
        // 左边的筷子 expect : 00011 ^ 00001 -> 00010
        // 右边的筷子 expect : 00010 ^ 00010 -> 00000
        return chopstick.compareAndSet(expect, expect ^ mask);
    }

    //尝试拿起左右筷子
    private boolean get(int mask) {
        int expect = chopstick.get();
        //只有当前筷子没有被拿起才返回 true, 并且更新筷子状态
        // 例如:1 号拿起筷子 00001 和 00010 , 更新后 : 00011 (说明1, 2号筷子被拿了)
        //  此时 2 号想拿起左边的筷子 : 00010 , & 运算发现被拿了, 返回false
        return (expect & mask) <= 0 && chopstick.compareAndSet(expect, expect ^ mask);
    }
}

方式③ 仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。

import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。
 */

class DiningPhilosophers {

    public DiningPhilosophers() {

    }

    //若 chopstick 等于 0, 表示筷子未被使用
    AtomicInteger chopstick = new AtomicInteger();
    // 00001, 00010, 00100, 01000, 10000
    int[] chopstickMask = new int[]{1, 2, 4, 8, 16};

    //进入临界区
    AtomicInteger mutex = new AtomicInteger();


    // 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 leftC = chopstickMask[philosopher];
        int rightC = chopstickMask[(philosopher + 1) % 5];
        //信号量 -1
        //进入临界区
        while (!mutex.compareAndSet(0, 1)) {
            Thread.sleep(1);
        }
        //尝试拿起左右筷子
        while (!get(leftC)) {
            Thread.sleep(1);
        }
        while (!get(rightC)) {
            Thread.sleep(1);
        }
        //退出临界区
        while (!mutex.compareAndSet(1, 0)) {
            Thread.sleep(1);
        }

        pickLeftFork.run();
        pickRightFork.run();
        //eat
        eat.run();
        putLeftFork.run();
        putRightFork.run();
        //放下左右筷子
        while (!put(leftC)) {
            Thread.sleep(1);
        }
        while (!put(rightC)) {
            Thread.sleep(1);
        }
        //信号量 + 1
    }

    private boolean put(int mask) {
        int expect = chopstick.get();
        //例如:1 号放下筷子 00001 和 00010
        // 左边的筷子 expect : 00011 ^ 00001 -> 00010
        // 右边的筷子 expect : 00010 ^ 00010 -> 00000
        return chopstick.compareAndSet(expect, expect ^ mask);
    }

    //尝试拿起左右筷子
    private boolean get(int mask) {
        int expect = chopstick.get();
        //只有当前筷子没有被拿起才返回 true, 并且更新筷子状态
        // 例如:1 号拿起筷子 00001 和 00010 , 更新后 : 00011 (说明1, 2号筷子被拿了)
        //  此时 2 号想拿起左边的筷子 : 00010 , & 运算发现被拿了, 返回false
        return (expect & mask) <= 0 && chopstick.compareAndSet(expect, expect ^ mask);
    }
}

4、总结

哲学家进餐问题的关键在于解决进程死锁

这些进程之间只存在互斥关系,但是与之前接触到的互斥关系不同的是,每个进程都需要同时持有两个临界资源,因此就有“死锁”问题的隐患。


如果在考试中遇到了一个进程需要同时持有多个临界资源的情况,应该参考哲学家问题的思想,分析题中给出的进程之间是否会发生循环等待,是否会发生死锁。

可以参考哲学家就餐问题解决死锁的三种思路。

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值