Java基础学习之并发篇:哲学家就餐问题
学习目标
哲学家就餐问题是在计算机科学中的一个经典问题,用来演示在并行计算中多线程同步时产生的问题。在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个必要条件:
- 互斥
- 占有且等待
- 不可抢占
- 循环等待
此时我们可以通过打破循环等待来解决死锁问题:
即通过简单的标志位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基础学习之并发篇:哲学家就餐问题相关推荐
- Java工程师学习指南 中级篇
Java工程师学习指南 中级篇 最近有很多小伙伴来问我,Java小白如何入门,如何安排学习路线,每一步应该怎么走比较好.原本我以为之前的几篇文章已经可以解决大家的问题了,其实不然,因为我写的文章都是站 ...
- Java工程师学习指南 入门篇
Java工程师学习指南 入门篇 最近有很多小伙伴来问我,Java小白如何入门,如何安排学习路线,每一步应该怎么走比较好.原本我以为之前的几篇文章已经可以解决大家的问题了,其实不然,因为我之前写的文章都 ...
- Java中大数据数组,Java基础学习笔记之数组详解
摘要:这篇Java开发技术栏目下的"Java基础学习笔记之数组详解",介绍的技术点是"java基础学习笔记.基础学习笔记.Java基础.数组详解.学习笔记.Java&qu ...
- 尚学堂JAVA基础学习笔记_2/2
尚学堂JAVA基础学习笔记_2/2 文章目录 尚学堂JAVA基础学习笔记_2/2 写在前面 第10章 IO技术 1. IO入门 2. IO的API 3. 装饰流 4. IO实战 5. CommonsI ...
- Java 基础学习-Java语言概述
Java 基础学习 第一章 Java语言概述 回顾java基础知识,进行整理记录. 文章目录 Java 基础学习 前言 一. Java语言发展史(了解) 二.Java语言跨平台原理(理解) 三.JRE ...
- JAVA基础学习精简心得笔记整理
JAVA基础学习精简心得笔记整理 配置java环境变量 Java的运行过程 基本数据类型 引用数据类型 逻辑运算符 数组 方法重载 封装 继承 多态 多态的作用 单例设计模式 接口interface ...
- java基础学习整理(一)
java基础学习整理(一) lesson1: D0s命令: 1.回到根目录,>cd \ 2.复制命令行下的内容,右击标记所要复制的内容,这样就已经复制好了,右击粘贴就可以了. 3.查看,设置环境 ...
- 【Java基础学习笔记】- Day11 - 第四章 引用类型用法总结
Java基础学习笔记 - Day11 - 第四章 引用类型用法总结 Java基础学习笔记 - Day11 - 第四章 引用类型用法总结 4.1 class作为成员变量 4.2 interface作为成 ...
- java基础学安卓开发_Android开发学习路线之Java基础学习
原标题:Android开发学习路线之Java基础学习 很多Android学习开发者刚入手Android开发技术时,稍微有点迫切.任何的开发技术都有其基础语言,Android应用程序开发是以Java语言 ...
最新文章
- WindowsServer2012史记4-重复数据删除的魅力
- fieldset在ie8下的margin\padding bug规避
- 【Spring学习】IoC、DI、AOP入门学习
- oracle报V27的错误解决办法,oracle11g ora-27154 past/wait 错误解决方法
- access 战地1不加入ea_炒牛肉时,想要牛肉嫩滑又不老,只需加入1样东西,很多人都不懂...
- python对象属性在引用时前面需要加()_python基础-面向对象进阶
- UIAlertController 大坑
- c#winform演练 ktv项目 MediaPlayer控件播放音乐
- NI强化半导体测试布局 弹性/高性价比打败不景气
- JAVA代码走查审查规范
- FNLP(FudanNLP)的安装及在eclipse中的使用
- pg和MySQL读性能_[评测]低配环境下,PostgresQL和Mysql读写性能简单对比
- HTML站内搜索引擎
- php电子邮件群发源码,php电子邮件群发源码
- Rebase Current onto Selected
- 读入一系列正整数数据,输入-1表示输入结束,-1本身不是输入的数据。程序输出读到的数据中的奇数和偶数的个数。
- 永磁体磁偏角测试原理和测量设备介绍
- echarts饼图自定义设置颜色的三种方式
- word邮件合并发送记录_如何将Word文档作为电子邮件正文发送
- 零基础学习MSP430F552LP开发板,学习前期准备,Code Composer Studio(CCS)软件的安装
热门文章
- mac设置mysql root密码_mac如何更改mysql root密码 Mac平台重新设置MySQL的root密码
- git提交本地项目gitlab合并分支提交代码合并分支时的冲突解决git拉取新项目
- 洛谷P2575 高手过招
- 带你深入了解Java!一、那些不为人所知的”秘密“!
- 截屏软件在截屏时窗口变大问题解决
- linux下mysql命令大全
- Occlusion Aware Facial Expression RecognitionUsing CNN With Attention Mechanism阅读笔记
- 掌控安全:安全领域知识图谱
- 坚果云一直显示分析的解决办法与ubuntu下坚果云选择性忽略的方法
- 乡下,已经不是衣锦还乡的去处了(转载)