学习目标

哲学家就餐问题是在计算机科学中的一个经典问题,用来演示在并行计算中多线程同步时产生的问题。在1971年,著名的计算机科学家艾兹格·迪科斯彻提出了一个同步问题,即假设有五台计算机都试图访问五份共享的磁带驱动器。稍后,这个问题被托尼·霍尔重新表述为哲学家就餐问题。这个问题可以用来解释死锁和资源耗尽

从百度百科上解释来看哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。总结来看哲学家问题就是:

接下来我们就将上述描述抽象出来,并用Java并发基础来解决哲学家就餐问题。


初步解决

针对上述问题我们可以简单定个协议规则:

  • 规则1:每个哲学家(线程)先拿起左边的叉子,再拿右边的叉子。
  • 规则2:如果拿不到就等待。

那么我可以抽象如下:

  • id(1-5)用来描述不同的哲学家
  • state三种状态:thinking、hungry、eating
  • 方法有拿起和放下叉子:takeLeft、takeRight、putLeft和putRight

那么我们可以得到如下程序:
哲学家抽象

/*** 哲学家抽象*/
public class Philosopher implements Runnable{public String getState() {return state;}public void setState(String state) {this.state = state;}String state;int id;// 用来统计哲学家完成的次数int count = 0;// 用来统计总共完成的次数static AtomicInteger total = new AtomicInteger(0);// 开始时间static long startMills = System.currentTimeMillis();public Philosopher(int id){this.id = id;this.state = "Thinking";}/*** 思考后状态是Hungry*/public void thinking() throws InterruptedException {if(this.state == "Thinking") {Thread.sleep((long)(Math.random()*100));this.state = "Hungry";}}/*** 修改状态为Eating并模拟一段时间*/public void eating() throws InterruptedException {this.state = "Eating";if(Math.random() > 0.9) {Thread.sleep(100000);} else {Thread.sleep((long)(Math.random()*100));}}public int left(){return this.id - 1;}public int right(){// %5是因为哲学家id是从1-5,方便下标操作return this.id % 5;}/*** 修拿起叉子需要判断当前叉子是否已被拿*/private boolean _take(int[] forks, int fork) {if(forks[fork] == 0) {forks[fork] = this.id;return true;}return false;}protected boolean takeLeft(int[] forks) {return this._take(forks, this.left());}protected boolean takeRight(int[] forks) {return this._take(forks, this.right());}protected void putRight(int[] forks) {if(forks[this.right()] == this.id) {forks[this.right()] = 0;}}protected void putLeft(int[] forks) {if(forks[this.left()] == this.id) {forks[this.left()] = 0;}}protected boolean checkLeft(int[] forks) {return forks[this.left()] == 0;}protected boolean checkRight(int[] forks) {return forks[this.right()] == 0;}public void finished(){count ++;int t = total.incrementAndGet();// 计算每秒完成就餐的哲学家数量double speed = (t * 1000.0) / (System.currentTimeMillis() - startMills);this.state = "Thinking";System.out.format("Philosopher %d finished %d times, speed = %.2f.\n",this.id,this.count,speed);}
}

模拟就餐

public class DiningPhilosophersDeadlock {// 定义5个哲学家Phi[] phis = new Phi[5];// 定义5把叉子volatile int[] forks = new int[5];public DiningPhilosophersDeadlock(){// 初始化for(int i = 0; i < 5; i++) {phis[i] = new Phi(i+1);forks[i] = 0;}}class Phi extends Philosopher {public Phi(int id) {super(id);}@Overrideprotected synchronized boolean takeLeft(int[] forks) {return super.takeLeft(forks);}@Overrideprotected synchronized boolean takeRight(int[] forks) {return super.takeRight(forks);}public void run(){while(true) {try {this.thinking();this.takeLeft(forks)Thread.sleep(100);this.takeRight(forks)this.eating();this.putLeft(forks);this.putRight(forks);this.finished();} catch (InterruptedException e) {e.printStackTrace();}}}}public void run(){ExecutorService pool = Executors.newFixedThreadPool(5);for(int i = 0; i< 5; i++) {pool.submit(phis[i]);}}public static void main(String[] argv) {DiningPhilosophersDeadlock solver = new DiningPhilosophersDeadlock();solver.run();}

通过模拟开启5个线程,运行发现发生了死锁,其实通过分析可以知道当5个哲学家(即5个线程)同时拿起叉子时,此时都在等待拿起右叉子,所有5个线程都在等待,陷入了死循环,这不发生死锁才怪。乍一看,我们可以通过简单的逻辑来处理,比如下面的:

     while(!this.takeLeft(forks)) {Thread.sleep(0);}Thread.sleep(100);int c = 0;while(!this.takeRight(forks)) {c++;if(c > 100) {this.putLeft(forks);continue;}Thread.sleep(0);}

我们知道死锁产生需要4个必要条件:

  1. 互斥
  2. 占有且等待
  3. 不可抢占
  4. 循环等待

此时我们可以通过打破循环等待来解决死锁问题

即通过简单的标志位c来判断,当拿不到右叉则累计达到100主动放下左叉
但重新运行程序,我们又会发现程序停止不动了,一波分析后发现可能产生了这种情况,当5个线程同时拿起左叉,后面又同时放下了左叉,不断同时拿起和放下,这便是所谓的活锁(livelock)

优化

当产生了上述活锁的问题后,我们不想去通过逻辑解决了,还是直接加锁吧。便有了下面的想法:
添加成员

   ReentrantLock lock = new ReentrantLock();Condition wait = lock.newCondition();

通过Lock解决

   this.thinking();lock.lockInterruptibly();this.takeLeft(forks)Thread.sleep(100);this.takeRight(forks)this.eating();this.putLeft(forks);this.putRight(forks);this.finished();lock.unlock();

但我们发现速度很慢:

"C:\Program Files\Java\jdk1.8.0_211\bin\java.exe" "-
Philosopher 5 finished 1 times, speed = 6.80.
Philosopher 4 finished 1 times, speed = 5.43.
Philosopher 2 finished 1 times, speed = 5.36.
Philosopher 3 finished 1 times, speed = 5.97.
Philosopher 1 finished 1 times, speed = 5.98.
Philosopher 5 finished 2 times, speed = 5.89.
Philosopher 4 finished 2 times, speed = 6.23.
Philosopher 2 finished 2 times, speed = 6.19.
Philosopher 3 finished 2 times, speed = 6.25.
Philosopher 1 finished 2 times, speed = 6.21.

这确实很慢了,每秒仅仅能处理很少的哲学家就餐,通过进一步分析可以发现,我们是不是可以先每次检查一下,没拿到就释放锁,没必要执行后面代码。当拿到左叉后,再拿不到右叉是不是可以主动去探测下这个右叉的状态,当已经不是Eating状态后,我们可以不等它放下,直接拿过来,那么可以做到如下优化:

       this.thinking();lock.lockInterruptibly();boolean takeLeft = this.checkLeft(forks);if(!takeLeft) {// 直接释放lock.unlock();continue;}this.takeLeft(forks);boolean takeRight = this.checkRight(forks);if(takeRight) {this.takeRight(forks);} else {int rid = this.right();Phi rPhi = phis[forks[rid] - 1];// 探测下,是不是直接可以传递if(dirty[rid] && rPhi.getState() != "Eating") {forks[rid] = this.id;dirty[rid] = false;} else {lock.unlock();continue;}}lock.unlock();this.eating();lock.lockInterruptibly();this.putLeft(forks);this.putRight(forks);dirty[this.left()] = true;dirty[this.right()] = true;lock.unlock();this.finished();

发现执行后,执行速度有了显著提升:

Philosopher 2 finished 1 times, speed = 8.06.
Philosopher 3 finished 1 times, speed = 23.53.
Philosopher 1 finished 1 times, speed = 20.13.
Philosopher 5 finished 1 times, speed = 14.60.
Philosopher 4 finished 1 times, speed = 23.92.
Philosopher 1 finished 2 times, speed = 20.69.
Philosopher 3 finished 2 times, speed = 21.94.
Philosopher 4 finished 2 times, speed = 19.37.
Philosopher 4 finished 3 times, speed = 15.76.
Philosopher 4 finished 4 times, speed = 16.05.

延迟队列实现

我们也可以通过延迟队列来实现,具体可以借助LinkedBlockingQueue完成队列的进出,延迟队列DelayQueue来实现延迟中断(即超时后主动退出,放下刀叉)可以有如下流程:

主要代码

 // 声明的变量Philosopher[] phis;volatile int forks[];// 工作队列LinkedBlockingQueue<Philosopher> workingQueue;// 待处理队列LinkedBlockingQueue<Philosopher> managerQueue;// 延迟队列DelayQueue<DelayInterruptingThread> delayQueue = new DelayQueue<>();
 /*** 处理待处理队列中的哲学家拿起刀叉*/class ContentionManager implements Runnable {@Overridepublic void run() {while(true) {try {Philosopher phi = managerQueue.take();if(phi.checkLeft(forks) && phi.checkRight(forks)) {phi.takeLeft(forks);phi.takeRight(forks);workingQueue.offer(phi);} else {managerQueue.offer(phi);}} catch (InterruptedException e) {e.printStackTrace();}}}}
 /*** 处理饥饿状态下的哲学家完成就餐*/class Worker implements Runnable {@Overridepublic void run() {while (true) {Philosopher phi = null;try{phi = workingQueue.take();if(phi.getState()=="Hungry") {DelayInterruptingThread delayItem = new DelayInterruptingThread(Thread.currentThread(), 1000);delayQueue.offer(delayItem);phi.eating();delayItem.commit();phi.putLeft(forks);phi.putRight(forks);phi.finished();workingQueue.offer(phi);} else {phi.thinking();managerQueue.offer(phi);}} catch (InterruptedException e) {if(phi != null) {phi.putLeft(forks);phi.putRight(forks);if(phi.getState() == "Eating") {phi.setState("Hungry");}managerQueue.offer(phi);}}}}}

Java基础学习之并发篇:哲学家就餐问题相关推荐

  1. Java工程师学习指南 中级篇

    Java工程师学习指南 中级篇 最近有很多小伙伴来问我,Java小白如何入门,如何安排学习路线,每一步应该怎么走比较好.原本我以为之前的几篇文章已经可以解决大家的问题了,其实不然,因为我写的文章都是站 ...

  2. Java工程师学习指南 入门篇

    Java工程师学习指南 入门篇 最近有很多小伙伴来问我,Java小白如何入门,如何安排学习路线,每一步应该怎么走比较好.原本我以为之前的几篇文章已经可以解决大家的问题了,其实不然,因为我之前写的文章都 ...

  3. Java中大数据数组,Java基础学习笔记之数组详解

    摘要:这篇Java开发技术栏目下的"Java基础学习笔记之数组详解",介绍的技术点是"java基础学习笔记.基础学习笔记.Java基础.数组详解.学习笔记.Java&qu ...

  4. 尚学堂JAVA基础学习笔记_2/2

    尚学堂JAVA基础学习笔记_2/2 文章目录 尚学堂JAVA基础学习笔记_2/2 写在前面 第10章 IO技术 1. IO入门 2. IO的API 3. 装饰流 4. IO实战 5. CommonsI ...

  5. Java 基础学习-Java语言概述

    Java 基础学习 第一章 Java语言概述 回顾java基础知识,进行整理记录. 文章目录 Java 基础学习 前言 一. Java语言发展史(了解) 二.Java语言跨平台原理(理解) 三.JRE ...

  6. JAVA基础学习精简心得笔记整理

    JAVA基础学习精简心得笔记整理 配置java环境变量 Java的运行过程  基本数据类型 引用数据类型 逻辑运算符 数组 方法重载 封装 继承 多态 多态的作用 单例设计模式 接口interface ...

  7. java基础学习整理(一)

    java基础学习整理(一) lesson1: D0s命令: 1.回到根目录,>cd \ 2.复制命令行下的内容,右击标记所要复制的内容,这样就已经复制好了,右击粘贴就可以了. 3.查看,设置环境 ...

  8. 【Java基础学习笔记】- Day11 - 第四章 引用类型用法总结

    Java基础学习笔记 - Day11 - 第四章 引用类型用法总结 Java基础学习笔记 - Day11 - 第四章 引用类型用法总结 4.1 class作为成员变量 4.2 interface作为成 ...

  9. java基础学安卓开发_Android开发学习路线之Java基础学习

    原标题:Android开发学习路线之Java基础学习 很多Android学习开发者刚入手Android开发技术时,稍微有点迫切.任何的开发技术都有其基础语言,Android应用程序开发是以Java语言 ...

最新文章

  1. WindowsServer2012史记4-重复数据删除的魅力
  2. fieldset在ie8下的margin\padding bug规避
  3. 【Spring学习】IoC、DI、AOP入门学习
  4. oracle报V27的错误解决办法,oracle11g ora-27154 past/wait 错误解决方法
  5. access 战地1不加入ea_炒牛肉时,想要牛肉嫩滑又不老,只需加入1样东西,很多人都不懂...
  6. python对象属性在引用时前面需要加()_python基础-面向对象进阶
  7. UIAlertController 大坑
  8. c#winform演练 ktv项目 MediaPlayer控件播放音乐
  9. NI强化半导体测试布局 弹性/高性价比打败不景气
  10. JAVA代码走查审查规范
  11. FNLP(FudanNLP)的安装及在eclipse中的使用
  12. pg和MySQL读性能_[评测]低配环境下,PostgresQL和Mysql读写性能简单对比
  13. HTML站内搜索引擎
  14. php电子邮件群发源码,php电子邮件群发源码
  15. Rebase Current onto Selected
  16. 读入一系列正整数数据,输入-1表示输入结束,-1本身不是输入的数据。程序输出读到的数据中的奇数和偶数的个数。
  17. 永磁体磁偏角测试原理和测量设备介绍
  18. echarts饼图自定义设置颜色的三种方式
  19. word邮件合并发送记录_如何将Word文档作为电子邮件正文发送
  20. 零基础学习MSP430F552LP开发板,学习前期准备,Code Composer Studio(CCS)软件的安装

热门文章

  1. mac设置mysql root密码_mac如何更改mysql root密码 Mac平台重新设置MySQL的root密码
  2. git提交本地项目gitlab合并分支提交代码合并分支时的冲突解决git拉取新项目
  3. 洛谷P2575 高手过招
  4. 带你深入了解Java!一、那些不为人所知的”秘密“!
  5. 截屏软件在截屏时窗口变大问题解决
  6. linux下mysql命令大全
  7. Occlusion Aware Facial Expression RecognitionUsing CNN With Attention Mechanism阅读笔记
  8. 掌控安全:安全领域知识图谱
  9. 坚果云一直显示分析的解决办法与ubuntu下坚果云选择性忽略的方法
  10. 乡下,已经不是衣锦还乡的去处了(转载)