哲学家进餐问题的死锁问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了哲学家进餐问题的死锁问题相关的知识,希望对你有一定的参考价值。

参考技术A

(1)破坏请求保持条件
利用原子思想完成。即只有拿起两支筷子的哲学家才可以进餐,否则,一支筷子也不拿。
解法一:利用AND机制实现第1位哲学家的活动描述为:
philosopher (int I)

while(true)

思考;
swait(chopstick[(I+1)]%5,chopstick[I]);
进餐;
Ssignal(chopstick[I],chopstick[(I+i)%5]);


解法二:利用记录型信号量机制实现在初始化中增加一个信号量定义:semaphore mutex=1:
第1位哲学家的活动描述:
philosopher (int I)  
while(true)

思考;
wait(mutex);
wait(stiCk[I]);
wait(Stick[(I+1)%5]);
Signal(mutex);
进餐;
signal(stick[I]);
Signal(Stick[(I+1)%5]);    
该方法将拿两只筷子的过程作为临界资源,一次只允许一个哲学家进入。
(2)破坏环路等待条件
在上述死锁问题中,哲学家对筷子资源的申请构成了有向环路,如图2所示。  
图2环路等待
解法一:奇数号哲学家先拿他左边的筷子,偶数号哲学家先拿他右边的筷子。这样破坏了同方向环路,一个哲学家拿到一只筷子后,就阻止了他邻座的一个哲学家吃饭。按此规定,将是1、2号哲学家竞争I号筷子;3、4号哲学家竞争4号筷子。两种算法描述如下:
1)第1个哲学家的活动:
philosopher (int I)

while(true)

思考;
If I%2==1 then
wait(Stick[I]);
wait(stick[(I+1)%5]);
进餐;
Signal(stick[I J);
signal(stick[(I+1)%5]);
e1Se
wait(stick[(I+1)%5]);
wait(stick[I]);
进餐;
signal(stick[(I+1)%5]);
Signal(stick[I]);
  
(2)第1个哲学家的活动:
philosopher(int I)

while(true)

思考;
wait(chopstick[I+(I%2)];
wait(chopstick[(I+(I+1)%2)%5])
进餐;
signal(chopstick[I+(I%2)]);
Signal(chopstick[(I+(I+1)%2)%5]);    
解法二:至多允许四位哲学家进餐,将最后一个哲学家停止申请资源,断开环路。最终能保证有一位哲学家能进餐,用完释放两只筷子,从而使更多的哲学家能够进餐。增加一个信号量定义semaphore count=4:算法描述第1个哲学家的活动:
philosopher (int I)

while(true)
思考;
wait(count);
wait(chopstiok[I]);
wait(chopstick[I+1]mod 5);
进餐;
signal(chopstick[I]);
signal(chopstick[I+1]mod 5)
signal(count);


解法三:哲学家申请资源总是按照资源序号先大后小的顺序,这样0.3号哲学家先右后左,但是4号哲学家
先左后右,改变方向,破坏了环路。算法描述第1个哲学家的活动:
philosopher(int I)

while(true)

思考;
if I>(I+1)%5 then
wait(chopstick[I]);
wait(chopstick[I+1]mod 5);
else
wait(chopstick[T+1]mod 5);
wait(chopstick[T]);/*哲学家总是先取最
大序号的筷子*/
进餐;
signal(chopstick[I]);
signal(chopstick[I+1]mod5);
  

Java并发编程死锁

哲学家进餐问题

并发执行带来的最棘手的问题莫过于死锁了,死锁问题中最经典的案例就是哲学家进餐问题:5个哲学家坐在一个桌子上,桌子上有5根筷子,每个哲学家的左手边和右手边各有一根筷子。示意图如下:

哲学家进餐问题

并发执行带来的最棘手的问题莫过于死锁了,死锁问题中最经典的案例就是哲学家进餐问题:5个哲学家坐在一个桌子上,桌子上有5根筷子,每个哲学家的左手边和右手边各有一根筷子。示意图如下:

技术分享图片

哲学家必须拿起左右两边的筷子才能进餐,如果他们同时拿起左手边的筷子,就会导致死锁。因为右手边的筷子被他右边的那位哲学家当成左手边的筷子拿起来了,这样一来这五位哲学家谁都没有办法进餐,他们死锁了。

让我们用代码模拟这个死锁:

class Philosopher implements Runnable {
    private int id;
    public Philosopher(int id) {
                this.id = id;
    }
    public void run() {
        int leftCsIndex = id;
        int rightCsIndex = (id+1)%5;
        synchronized(PhiloTest.chopsticks[leftCsIndex]) {
            System.out.println("I got left chopstick");
            try { Thread.sleep(100); } catch (Exception e) {}
            synchronized(PhiloTest.chopsticks[rightCsIndex]) {
                System.out.println("I got right chopstick");
                System.out.println("Philosopher"+ id+": eating");
            }
        }
    }
}
public class PhiloTest {
    public static Object[] chopsticks = new Object[5];
    public static void main(String[] args) {
        for(int i=0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i=0; i < 5; i++) {
            exec.execute(new Philosopher(i));
        }
        exec.shutdown();
    }
}

输出结果如下,并且程序始终没有退出:

Philosopher0:I got left chopstick

Philosopher2:I got left chopstick

Philosopher1:I got left chopstick

Philosopher3:I got left chopstick

Philosopher4:I got left chopstick

我们创建了一个长度为5的数组,用来模拟筷子。此外我们定义了“哲学家线程”,每个哲学家都有自己的编号,我们假定哲学家左边的筷子对应的是数组中索引和哲学家编号相同的对象,哲学家右边的筷子对应的是数组中索引为哲学家编号加一的对象(注:第4个哲学家右手边的筷子对应数组中第0个对象)。每个哲学家都先拿起左边的筷子,为了保证所有的哲学家都拿到了左边的筷子,每个哲学家拿到左边的筷子后都等待100毫秒,然后再拿起右边的筷子,这时他们死锁了。

死锁的条件

死锁发生有四个条件,必须每个条件都满足才有可能发生死锁,只要破坏其中一个条件就不会死锁。

1互斥:线程申请获得的资源不能共享。在上面的例子中,每个哲学家不和别的哲学家共用一根筷子,反应在代码上就是每个“哲学家线程”用锁实现了互斥,一个哲学家拿到了对象的锁,其它哲学家就不能拿到这个对象的锁了。

2.持有并等待:线程在申请其它资源的时候不释放已经持有的资源。在上面的例子中,哲学家在试图去取右边筷子的时候同时持有左边的筷子。

3.不能抢占:线程持有的资源不能被其它线程抢占。在上面例子中,哲学家只能拿桌子上的筷子,不能从其它哲学家手里抢筷子用。

4.循环等待:在上面的例子中,第0个哲学家在等待第1个哲学家放下筷子,第1个哲学家等第2个哲学家放下筷子....第4个哲学家等待第0个哲学家放下筷子,如此就形成了循环等待。

避免死锁

避免死锁最简单的方法就是打破循环等待,比如5个哲学家中有一个哲学家先去拿右边的筷子,再拿左边的筷子,这样就破坏了循环等待。实例代码如下:

public class SolveDeadLock {
    public static Object[] chopsticks = new Object[5];
    public static void main(String[] args) {
        for(int i=0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i=0; i < 4; i++) {
            exec.execute(new Philosopher(i));
        }
        exec.shutdown();
        int leftCsIndex = 4;
        int rightCsIndex = 0;
        synchronized(SolveDeadLock.chopsticks[rightCsIndex]) {
            System.out.println("Philosopher4:I got right chopstick");
            try { Thread.sleep(100); } catch (Exception e) {}
            synchronized(SolveDeadLock.chopsticks[leftCsIndex]) {
                System.out.println("Philosopher4:I got left chopstick");
                System.out.println("Philosopher4: eating");
            }
        }
    }
}

输出结果:

Philosopher0:I got left chopstick

Philosopher2:I got left chopstick

Philosopher1:I got left chopstick

Philosopher3:I got left chopstick

Philosopher3:I got right chopstick

Philosopher3: eating

Philosopher2:I got right chopstick

Philosopher2: eating

Philosopher1:I got right chopstick

Philosopher1: eating

Philosopher0:I got right chopstick

Philosopher0: eating

Philosopher4:I got right chopstick

Philosopher4:I got left chopstick

Philosopher4: eating

上面的例子中我们修改了main()方法,使用主线程作为第4个哲学家,第四个哲学家先拿右面的筷子,再拿左面的筷子。这样就避免了循环等待,因此这次没有发生死锁。在哲学家进餐案例中,互斥和持有并等待是不能规避的,因为这两个是逻辑要求的,比如两个哲学家同时使用一根筷子是违背常识的。因此除了第四个条件外,我们还可以通过抢占来规避死锁。比如:设计一个“粗鲁的哲学家”,这个哲学家如果没有拿到筷子,就会去别的哲学家手里面抢筷子,这样就可以保证这个哲学家肯定可以吃到饭,一旦他放下筷子,就只有4个哲学家需要吃饭,而桌子上有5根筷子,这时肯定不会死锁。由于篇幅原因,这里就不使用代码实现了,感兴趣的读者可以试着实现这个想法。

总结

在多线程系统中,许多概率性的问题是由于线程之间发生了死锁,死锁导致一些线程永远都不会停止执行,虽然这些线程一直处于阻塞状态,但是仍然占用内存空间,这样就导致线程所占的内存空间永远不会被释放,这就是传说中的内存泄露。线程死锁是导致Java应用程序发生内存泄露的一个重要原因。因此在编写代码时一定要避免发生死锁,避免死锁最简单的方法就是对资源进行排序,所有线程对资源的访问都按照顺序获取,这样就避免了循环等待,从而避免死锁。

公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。

哲学家必须拿起左右两边的筷子才能进餐,如果他们同时拿起左手边的筷子,就会导致死锁。因为右手边的筷子被他右边的那位哲学家当成左手边的筷子拿起来了,这样一来这五位哲学家谁都没有办法进餐,他们死锁了。

让我们用代码模拟这个死锁:

class Philosopher implements Runnable {
    private int id;
    public Philosopher(int id) {
                this.id = id;
    }
    public void run() {
        int leftCsIndex = id;
        int rightCsIndex = (id+1)%5;
        synchronized(PhiloTest.chopsticks[leftCsIndex]) {
            System.out.println("I got left chopstick");
            try { Thread.sleep(100); } catch (Exception e) {}
            synchronized(PhiloTest.chopsticks[rightCsIndex]) {
                System.out.println("I got right chopstick");
                System.out.println("Philosopher"+ id+": eating");
            }
        }
    }
}
public class PhiloTest {
    public static Object[] chopsticks = new Object[5];
    public static void main(String[] args) {
        for(int i=0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i=0; i < 5; i++) {
            exec.execute(new Philosopher(i));
        }
        exec.shutdown();
    }
}

输出结果如下,并且程序始终没有退出:

Philosopher0:I got left chopstick

Philosopher2:I got left chopstick

Philosopher1:I got left chopstick

Philosopher3:I got left chopstick

Philosopher4:I got left chopstick

我们创建了一个长度为5的数组,用来模拟筷子。此外我们定义了“哲学家线程”,每个哲学家都有自己的编号,我们假定哲学家左边的筷子对应的是数组中索引和哲学家编号相同的对象,哲学家右边的筷子对应的是数组中索引为哲学家编号加一的对象(注:第4个哲学家右手边的筷子对应数组中第0个对象)。每个哲学家都先拿起左边的筷子,为了保证所有的哲学家都拿到了左边的筷子,每个哲学家拿到左边的筷子后都等待100毫秒,然后再拿起右边的筷子,这时他们死锁了。

死锁的条件

死锁发生有四个条件,必须每个条件都满足才有可能发生死锁,只要破坏其中一个条件就不会死锁。

1互斥:线程申请获得的资源不能共享。在上面的例子中,每个哲学家不和别的哲学家共用一根筷子,反应在代码上就是每个“哲学家线程”用锁实现了互斥,一个哲学家拿到了对象的锁,其它哲学家就不能拿到这个对象的锁了。

2.持有并等待:线程在申请其它资源的时候不释放已经持有的资源。在上面的例子中,哲学家在试图去取右边筷子的时候同时持有左边的筷子。

3.不能抢占:线程持有的资源不能被其它线程抢占。在上面例子中,哲学家只能拿桌子上的筷子,不能从其它哲学家手里抢筷子用。

4.循环等待:在上面的例子中,第0个哲学家在等待第1个哲学家放下筷子,第1个哲学家等第2个哲学家放下筷子....第4个哲学家等待第0个哲学家放下筷子,如此就形成了循环等待。

避免死锁

避免死锁最简单的方法就是打破循环等待,比如5个哲学家中有一个哲学家先去拿右边的筷子,再拿左边的筷子,这样就破坏了循环等待。实例代码如下:

public class SolveDeadLock {
    public static Object[] chopsticks = new Object[5];
    public static void main(String[] args) {
        for(int i=0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i=0; i < 4; i++) {
            exec.execute(new Philosopher(i));
        }
        exec.shutdown();
        int leftCsIndex = 4;
        int rightCsIndex = 0;
        synchronized(SolveDeadLock.chopsticks[rightCsIndex]) {
            System.out.println("Philosopher4:I got right chopstick");
            try { Thread.sleep(100); } catch (Exception e) {}
            synchronized(SolveDeadLock.chopsticks[leftCsIndex]) {
                System.out.println("Philosopher4:I got left chopstick");
                System.out.println("Philosopher4: eating");
            }
        }
    }
}

输出结果:

Philosopher0:I got left chopstick

Philosopher2:I got left chopstick

Philosopher1:I got left chopstick

Philosopher3:I got left chopstick

Philosopher3:I got right chopstick

Philosopher3: eating

Philosopher2:I got right chopstick

Philosopher2: eating

Philosopher1:I got right chopstick

Philosopher1: eating

Philosopher0:I got right chopstick

Philosopher0: eating

Philosopher4:I got right chopstick

Philosopher4:I got left chopstick

Philosopher4: eating

上面的例子中我们修改了main()方法,使用主线程作为第4个哲学家,第四个哲学家先拿右面的筷子,再拿左面的筷子。这样就避免了循环等待,因此这次没有发生死锁。在哲学家进餐案例中,互斥和持有并等待是不能规避的,因为这两个是逻辑要求的,比如两个哲学家同时使用一根筷子是违背常识的。因此除了第四个条件外,我们还可以通过抢占来规避死锁。比如:设计一个“粗鲁的哲学家”,这个哲学家如果没有拿到筷子,就会去别的哲学家手里面抢筷子,这样就可以保证这个哲学家肯定可以吃到饭,一旦他放下筷子,就只有4个哲学家需要吃饭,而桌子上有5根筷子,这时肯定不会死锁。由于篇幅原因,这里就不使用代码实现了,感兴趣的读者可以试着实现这个想法。

总结

在多线程系统中,许多概率性的问题是由于线程之间发生了死锁,死锁导致一些线程永远都不会停止执行,虽然这些线程一直处于阻塞状态,但是仍然占用内存空间,这样就导致线程所占的内存空间永远不会被释放,这就是传说中的内存泄露。线程死锁是导致Java应用程序发生内存泄露的一个重要原因。因此在编写代码时一定要避免发生死锁,避免死锁最简单的方法就是对资源进行排序,所有线程对资源的访问都按照顺序获取,这样就避免了循环等待,从而避免死锁。

公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。

以上是关于哲学家进餐问题的死锁问题的主要内容,如果未能解决你的问题,请参考以下文章

操作系统结合哲学家进餐问题分析如何预防死锁

Java总结—实现Runnable接口创建线程,线程安全同步,死锁(哲学家进餐问题),读写锁

面试17解析-死锁

哲学家进餐问题

经典进程的同步问题之——哲学家进餐

扫盲贴:死锁活锁饥饿