哲学家问题(图形化实现)
记得大二时学操作系统,有个关于锁的经典模型——哲学家模型。当时老师要求我们用程序实现,我当时没做出来,一直耿耿于怀。今天看Java?cookbook中的线程介绍。于是动手来试下,花了一天才弄出来。在此把大概过程罗列如下,也算是交几年前的作业吧。
?
程序的设计分两部分:哲学家问题的逻辑处理、图形化展示结果部分。最终效果如下:
图1:初始时五个哲学家都在睡眠
图2::哲学家0、3在睡眠,1、4在吃饭,2挨饿
图3:哲学家0、3在吃饭,1在挨饿,2、4在睡眠
首先做一点约定:
对于哲学家按顺时针计算,最顶部的哲学家为0,右边的为1,右下角为2……。
对于筷子见图1,哲学家0、1夹着的筷子为0,按顺时针递增,即1、2中间的筷子为1……
?
至于什么是哲学家问题,若忘记了可以翻阅下操作系统温习下,简单的描述下:有五个哲学家要吃饭,同时只有五个筷子,为了吃饭必须同时获得两个筷子。如果每个哲学家都是先拿左边筷子再拿右边筷子,则必然发生死锁。所以规定了奇数的先拿左边,偶数的先拿右边。由此可见,筷子是竞争资源。准确的讲,
筷子0是哲学家0、1竞争的资源;
筷子1是哲学家1、2竞争的资源;
筷子2是哲学家2、3竞争的资源;
筷子3是哲学家3、4竞争的资源;
筷子4是哲学家4、0竞争的资源;
?
类图如下:
所有的逻辑都在Person中完成。Person是一个继承了Thread的线程。在PhilosopherDining的main方法中实例化了五个Person并start了他们。现在我们看下Person的run方法及其涉及的其他方法:
/*** 不断的睡眠、吃饭.* * @see java.lang.Thread#run()*/public void run() { while (!done) { sleeping(); eat(); }}/*** 睡眠几秒钟.*/public void sleeping() {try { // id睡觉 guiMgr.setPersonSleeping(id); System.out.println("Person[" + id + "] is sleeping..."); Thread.sleep(SLEEP_TIME); } catch (InterruptedException e) { e.printStackTrace(); }}/*** 吃饭,必须同时得到左右筷子才能吃.*/public void eat() { // 奇数的先拿左边 if (isOdd(id)) { // 先拿左边 getChopstick(leftNo); // 再拿右边 getChopstick(rightNo); } else { // 偶数的先拿右边 // 先拿右边 getChopstick(rightNo); // 再拿左边 getChopstick(leftNo); } // 两边都拿到的话则可以放心的吃东西了 eating();}
?
很简单,先睡眠几分钟,饿醒了就吃东西。吃东西又需要先拿左/右边的筷子再拿右/左边的筷子,然后才能吃。关键就在拿筷子getChopstick、吃饭eating这两件事上。
/*** 试图获取筷子,若被他人先拿则需等待.* * @param chopstickNo* 要拿的筷子号*/private void getChopstick(final int chopstickNo) { System.out.println("id:" + id + " no:" + chopstickNo); synchronized (chopsticks[chopstickNo]) { while (chopsticks[chopstickNo].isUsed()) { System.out.println("Person[" + id + "] is waiting Chopstick[" + chopstickNo + "]"); try { // 吃不到东西,哭了 guiMgr.setPersonCrying(id); chopsticks[chopstickNo].wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } // 获取得到筷子了 guiMgr.setChopstickImage(id, chopstickNo); // 占用筷子 chopsticks[chopstickNo].setUsed(true); System.out.println("Person[" + id + "] has got Chopstick[" + chopstickNo + "] at time:" + new Date());}
?
请先忽略所有与guiMgr相关的代码,那是为此程序加上GUI界面的代码,与逻辑没有任何关系。
首先某个特定的筷子是竞争资源,只有它未被占用时才可持有,也就是上面的while语句。如果一直被Used,则一直wait。直到被notifyAll则可继续下面的代码,即把筷子占用chopsticks[chopstickNo].setUsed(true);
?
接下来是吃的动作
/*** 吃几秒钟.*/private void eating() { System.out.println("Person[" + id + "] is eating using Chopsticks[" + leftNo + "][" + rightNo + "]"); try { // 得到了两只筷子,可以吃东西了 guiMgr.setPersonEating(id); Thread.sleep(SLEEP_TIME); } catch (InterruptedException e) { e.printStackTrace(); } // 吃完释放左右筷子 synchronized (chopsticks[leftNo]) { // 释放筷子 guiMgr.releaseChopstick(leftNo); chopsticks[leftNo].setUsed(false); // 唤醒其他需要此筷子的哲学家 chopsticks[leftNo].notifyAll(); } synchronized (chopsticks[rightNo]) { // 释放筷子 guiMgr.releaseChopstick(rightNo); chopsticks[rightNo].setUsed(false); chopsticks[rightNo].notifyAll(); }}
?
吃饭大概几秒钟时间,然后把左右筷子释放,并唤醒等待该筷子的其他哲学家。至此,哲学家问题就搞定了。接下来看看GUI图像实现部分。
?
本例采用了组合的方式即PhilosopherDining中含有GuiManage的引用。GuiManage是专门用于转换哲学家问题到GUI图像显示的中间类,它又含有图像实现类JFrameGridLayout的引用,根据PhilosopherDining中Person实例传递的参数做判断,调用JFrameGridLayout暴露出的方法来控制GUI界面。从而剥离了业务逻辑与前端显示的耦合。
大体思路如下。我使用的是GridLayout,即网格布局。
布局的各组件如下。其中最外层是数字,表示坐标,灰色部分才是真正的布局区域,P表示哲学家,C表示筷子。
P0~P4分布在五个相对对称的位置;
筷子的位置因为会经常变化,所以使用复合数字及不同底色表示。其中
红色表示筷子的初始位置,即每个筷子未被任何人持有的情况下的位置;
蓝色表示筷子相对于其初始位置的右边;
黄色表示筷子相对于其初始位置的左边;
?
这时再看GuiManage中的下面常量就好理解了/*** 人的位置,固定的.*/private static final Position[] personPositions = new Position[MAX];static {personPositions[0] = new Position(0, 5);personPositions[1] = new Position(4, 10);personPositions[2] = new Position(9, 9);personPositions[3] = new Position(9, 1);personPositions[4] = new Position(4, 0);}/*** 筷子位置,枚举了筷子三种状态的位置. */private static final ChopstickPosition[] chopstickPositions = new ChopstickPosition[MAX];static {// 枚举五个筷子的十五种状态下对应的位置chopstickPositions[0] = new ChopstickPosition(new int[][] { { 1, 6 }, { 3, 9 }, { 3, 7 } });chopstickPositions[1] = new ChopstickPosition(new int[][] { { 5, 9 }, { 8, 9 }, { 6, 7 } });chopstickPositions[2] = new ChopstickPosition(new int[][] { { 9, 8 }, { 9, 2 }, { 7, 5 } });chopstickPositions[3] = new ChopstickPosition(new int[][] { { 8, 1 }, { 5, 1 }, { 6, 3 } });chopstickPositions[4] = new ChopstickPosition(new int[][] { { 3, 1 }, { 1, 4 }, { 3, 3 } });
?
还有这段
/** * 根据筷子号码,及被谁持有设置筷子图片. * * @param id * 人的id * @param no * 筷子号码*/public void setChopstickImage(final int id, final int no) { Position p = null; String image = null; if (id % MAX == no) { p = chopstickPositions[no].getPosition(RIGHT_INDEX); image = getChopstickImage(no, RIGHT_INDEX); } else { p = chopstickPositions[no].getPosition(LEFT_INDEX); image = getChopstickImage(no, LEFT_INDEX); } setChopstickImage(no, p, image);}
??
id?%?MAX?==?no则是相对于筷子原位置的右边,这是很好理解的。如现在是哲学家0持有0号筷子则0?%?5?==?0是成立的,则0号筷子处于C00?即(1,6)的位置,(1,6)则为chopstickPositions[0]的第0(RIGHT_INDEX的值)个元素,反正则是C01,这则是chopstickPositions[0]的第1(LEFT_INDEX的值)个元素。
理解了此处则整个画图程序也就基本理解了。
注:C00中第一个数字表示第0号筷子,第二个数字表示是RIGHT_INDEX、LEFT_INDEX或INIT_INDEX的其中一个值。
?
?