欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

哲学家就餐问题

程序员文章站 2022-04-20 11:53:17
...

 本文对多线程中经典问题-哲学家就餐问题用Java语言描述,给出了三种常见的解决方法,实现了其中的两种。


一、问题描述:


在一个圆桌上,有n个哲学家,n只筷子,每个哲学家左右两边各返一只筷子。哲学家可以进行思考和吃饭,思考时,不获取筷子。吃饭时,必须同时获得左右两只筷子才能吃饭(先获取左边的再获取右边的)。

哲学家就餐问题

二、Java实现哲学家就餐问题

 

实现Chopstick类,每个筷子有个编号index,还有个属性isUsing表示是否正在使用,如果isUsing为true,后访问的哲学家对象就必须等待直到isUsing为false才能访问

package Blog.philosopher;

public class Chopstick {
    private int index;
    private boolean isUsing = false;

    public Chopstick(int index){
        this.index = index;
    }

    //修改toString方法,因为System.out.println()输出对象都是调用toString方法
    @Override
    public String toString() {
        return "Chopstick [index = " + index + "]";
    }

    //对于一个成员方法加synchronized关键字,作用范围是从调用该方法开始,作用对象是该方法所在的对象
    public synchronized void take() throws InterruptedException{
        while(isUsing){
            wait();
        }
        isUsing = true;
    }

    //使用结束,通知所有其他线程
    public synchronized void drop(){
        isUsing = false;
        notifyAll();
    }

    public int getIndex() {
        return index;
    }

    public boolean getIsUsing(){
        return isUsing;
    }
}

实现Philosopher类

package Blog.philosopher;

public class Philosopher implements Runnable{
    protected Chopstick left;
    protected Chopstick right;
    protected int index;
    protected int thinkTime;
    protected int eatTime;

    public Philosopher(Chopstick left, Chopstick right, int index) {
        this.left = left;
        this.right = right;
        this.index = index;
    }

    //sleep()方法让出CPU,不释放资源。
    public void thinking() throws InterruptedException{
        System.out.println(this + "thinking");
        Thread.sleep(thinkTime);
    }

    public void eating() throws InterruptedException{
        System.out.println(this + "eating");
        Thread.sleep(eatTime);
    }

    @Override
    public void run() {
        try{
            while ( !Thread.interrupted() ){
                thinking();

                System.out.println(this + "take the left");
                left.take();
                Thread.sleep(10);
                System.out.println(this + "going to take the right");
                right.take();
                System.out.println(this + "has took the right");
                eating();

                left.drop();
                right.drop();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public String toString() {
        return "Philosopher [index = " + index + "]";
    }
}


哲学家先进行思考,思考结束先获取左边的筷子,如果获取了左边的,再获取右边的筷子,如果没有获得则一直等待直到获得为止,获得到左右两只筷子随后进行吃,吃完则继续思考,继续循环。

修改:这里为了看到现象,可以将思考时间和吃饭时间设置为0,更加容易出现死锁

还有可以在取左筷子和右筷子间添加一段sleep()时间,虽然不符合正常人(谁会拿了左筷子再sleep然后拿右筷子),但是符合操作系统,因为某个线程获取资源A,会进行一小段时间访问,访问之后才会去访问资源B。

 

测试类:


package Blog.philosopher;

public class PhilosopherTest {
    public static void main(String[] args) {
        int size=5;
        Chopstick[] chopstick = new Chopstick[size];
        for(int i=0;i<size;i++){
            chopstick[i] = new Chopstick(i);
        }
        for(int i=0;i<size;i++){
            Thread thread = new Thread(new Philosopher(chopstick[(i+1)%size], chopstick[i], i));
            thread.start();
            //这里不能用join()方法,因为这会等待该进程结束了才会调用主进程,创造余下的进程
            //thread.join();
        }
        System.out.println("main线程已经结束");
    }
}


 

结果:出现了死锁

main线程已经结束
Philosopher [index = 0]thinking
Philosopher [index = 1]thinking
Philosopher [index = 0]take the left
Philosopher [index = 2]thinking
Philosopher [index = 1]take the left
Philosopher [index = 3]thinking
Philosopher [index = 4]thinking
Philosopher [index = 2]take the left
Philosopher [index = 3]take the left
Philosopher [index = 4]take the left
Philosopher [index = 0]going to take the right
Philosopher [index = 1]going to take the right
Philosopher [index = 2]going to take the right
Philosopher [index = 4]going to take the right
Philosopher [index = 3]going to take the right

 

三、解决方法

先看死锁的四个必要条件:

l  互斥

l  请求保持

l  不可剥夺

l  循环等待

要解决死锁,就至少要破坏其中一个条件,然后“互斥”一般是不能破坏的,所以针对其他三个条件采用合适的方法。

 

1.       资源分级解法:

另一个简单的解法是为资源(这里是餐叉)分配一个偏序或者分级的关系,并约定所有资源都按照这种顺序获取,按相反顺序释放,而且保证不会有两个无关资源同时被同一项工作所需要。在哲学家就餐问题中,资源(餐叉)按照某种规则编号为15,每一个工作单元(哲学家)总是先拿起左右两边编号较低的餐叉,再拿编号较高的。用完餐叉后,他总是先放下编号较高的餐叉,再放下编号较低的。破坏了循环等待条件。

代码:

继承了Philosopher为Philosopher2,重写了PhilosopherTest为PhilosopherTest2

 

package Blog.philosopher;

public class Philosopher2 extends Philosopher{
    //构造方法
    public Philosopher2(Chopstick left, Chopstick right, int index) {
        super(left, right, index);
    }

    @Override
    public void run() {
        try{
            while ( !Thread.interrupted() ){
                thinking();

                System.out.println(this + "take the small");
                Chopstick small = left.getIndex() < right.getIndex() ? left : right;
                small.take();

                Thread.sleep(10);

                Chopstick big = left.getIndex() > right.getIndex() ? left : right;
                System.out.println(this + "going to take the big chopstick" + big.getIndex());
                big.take();
                System.out.println(this + "has took the big right" + big.getIndex());
                eating();

                big.drop();
                small.drop();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

package Blog.philosopher;

public class PhilosopherTest2 {
    public static void main(String[] args) {
        int size=5;
        int thinkTime= 0;
        int eatTime = 0;
        Chopstick[] chopstick = new Chopstick[size];
        for(int i=0;i<size;i++){
            chopstick[i] = new Chopstick(i);
        }
        for(int i=0;i<size;i++){
            Thread thread = new Thread(new Philosopher2(chopstick[(i+1)%size], chopstick[i], i));
            thread.start();
            //这里不能用join()方法,因为这会等待该进程结束了才会调用主进程,创造余下的进程
            //thread.join();
        }
        System.out.println("main线程已经结束");
    }

}


2.       服务生解法:

一个简单的解法是引入一个餐厅服务生,哲学家必须经过他的允许才能拿起餐叉。因为服务生知道哪只餐叉正在使用,所以他能够作出判断避免死锁。可以采用一次性分配资源的方法来避免死锁。破坏请求保持条件,线程不可能保持资源的同时继续申请新的资源。代码中我没有引入服务生,采用了一种简化,直接在Philosopher3中利用哲学家自己判断,如果右筷子可用则去获取,如果不可用则放弃左筷子。

代码:

重写了Philosopher为Philosopher3,重写了PhilosopherTest为PhilosopherTest3

package Blog.philosopher;

public class Philosopher3 extends Philosopher{

    public Philosopher3(Chopstick left, Chopstick right, int index) {
        super(left, right, index);
    }

    @Override
    public void run() {
        try{
            while ( !Thread.interrupted() ){
                thinking();

                System.out.println(this + "take the left");
                left.take();

                Thread.sleep(10);

                System.out.println(this + "going to take the right");
                //如果右筷子正在使用,则放弃左筷子
                if(right.getIsUsing()){
                    left.drop();
                    System.out.println(this + "give up");
                }else {
                    right.take();
                    System.out.println(this + "has took the right");
                    eating();

                    left.drop();
                    right.drop();
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

package Blog.philosopher;

public class PhilosopherTest3 {
    public static void main(String[] args) {
        int size=5;
        int thinkTime= 10;
        int eatTime = 10;
        Chopstick[] chopstick = new Chopstick[size];
        for(int i=0;i<size;i++){
            chopstick[i] = new Chopstick(i);
        }
        for(int i=0;i<size;i++){
            Thread thread = new Thread(new Philosopher3(chopstick[(i+1)%size], chopstick[i], i));
            thread.start();
        }

        //提供一个服务生

        System.out.println("main线程已经结束");
    }
}


3.       Chandy/Misra解法:

1984年,K. Mani ChandyJ. Misra提出了哲学家就餐问题的另一个解法,允许任意的用户(编号P1, ..., Pn)争用任意数量的资源。与迪科斯彻的解法不同的是,这里编号可以是任意的。

1.对每一对竞争一个资源的哲学家,新拿一个餐叉,给编号较低的哲学家。每只餐叉都是干净的或者脏的。最初,所有的餐叉都是脏的。

2.当一位哲学家要使用资源(也就是要吃东西)时,他必须从与他竞争的邻居那里得到。对每只他当前没有的餐叉,他都发送一个请求。

3.当拥有餐叉的哲学家收到请求时,如果餐叉是干净的,那么他继续留着,否则就擦干净并交出餐叉。

4.当某个哲学家吃东西后,他的餐叉就变脏了。如果另一个哲学家之前请求过其中的餐叉,那他就擦干净并交出餐叉。

这个解法允许很大的并行性,适用于任意大的问题。

这个方法略复杂,我没有实现,可以参考以下链接

http://bbs.csdn.net/topics/390754385?page=1