java中的哲学家用餐问题-Java快速进阶教程
1. 简介
哲学家用餐问题是用于描述多线程环境中的同步问题并说明解决这些问题的技术的经典问题之一。Dijkstra首先提出了这个问题,并提出了关于访问磁带驱动器外围设备的计算机的问题。
目前的公式是由Tony Hoare给出的,他也以发明快速排序算法而闻名。在本文中,我们分析了这个众所周知的问题并编写了一个流行的解决方案。
2. 问题
上图表示了问题。有五个沉默的哲学家(P1 – P5)围坐在一张圆桌旁,一生都在吃饭和思考。
有五个叉子供他们分享(1 – 5),为了能够吃东西,哲学家需要双手拿叉子。吃完饭后,他把它们都放下,然后它们就可以被另一个重复相同循环的哲学家挑选出来。
目标是提出一个方案/协议,帮助哲学家实现他们的饮食和思考目标,而不会被饿死。
3. 解决方案
最初的解决方案是让每个哲学家遵循以下协议:
while(true) {
// Initially, thinking about life, universe, and everything
think();
// Take a break from thinking, hungry now
pick_up_left_fork();
pick_up_right_fork();
eat();
put_down_right_fork();
put_down_left_fork();
// Not hungry anymore. Back to thinking!
}
正如上面的伪代码所描述的,每个哲学家最初都在思考。一段时间后,哲学家感到饥饿并希望吃东西。
这时,他伸手拿起两边的叉子,一旦他得到了两个叉子,就继续吃饭。吃完饭后,哲学家就会把叉子放下,这样它们就可以给他的邻居了。
4. 实施
我们将每个哲学家建模为实现Runnable接口的类,以便我们可以将它们作为单独的线程运行。每个哲学家都可以进入他的左右两侧的两个叉子:
public class Philosopher implements Runnable {
// The forks on either side of this Philosopher
private Object leftFork;
private Object rightFork;
public Philosopher(Object leftFork, Object rightFork) {
this.leftFork = leftFork;
this.rightFork = rightFork;
}
@Override
public void run() {
// Yet to populate this method
}
}
我们还有一种方法可以指示哲学家执行一个动作——吃东西、思考或获取叉子来准备吃饭:
public class Philosopher implements Runnable {
// Member variables, standard constructor
private void doAction(String action) throws InterruptedException {
System.out.println(
Thread.currentThread().getName() + " " + action);
Thread.sleep(((int) (Math.random() * 100)));
}
// Rest of the methods written earlier
}
如上面的代码所示,每个操作都是通过将调用线程挂起随机一段时间来模拟的,这样执行顺序就不会仅由时间强制执行。
现在,让我们实现哲学家的核心逻辑。
为了模拟获取分叉,我们需要锁定它,这样就不会有两个哲学家线程同时获取它。
为了实现这一点,我们使用sync关键字来获取 fork 对象的内部监视器,并防止其他线程执行相同的操作。可以在此处找到 Java 中同步关键字的指南。我们现在继续在Philosopher类中实现run() 方法:
public class Philosopher implements Runnable {
// Member variables, methods defined earlier
@Override
public void run() {
try {
while (true) {
// thinking
doAction(System.nanoTime() + ": Thinking");
synchronized (leftFork) {
doAction(
System.nanoTime()
+ ": Picked up left fork");
synchronized (rightFork) {
// eating
doAction(
System.nanoTime()
+ ": Picked up right fork - eating");
doAction(
System.nanoTime()
+ ": Put down right fork");
}
// Back to thinking
doAction(
System.nanoTime()
+ ": Put down left fork. Back to thinking");
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
这个方案完全实现了前面描述的方案:一个哲学家思考了一会儿,然后决定吃东西。
之后,他左右拿起叉子,开始吃东西。完成后,他把叉子放下。我们还为每个操作添加时间戳,这将有助于我们了解事件发生的顺序。
为了启动整个过程,我们编写了一个客户端,该客户端创建 5个哲学家作为线程并启动所有线程:
public class DiningPhilosophers {
public static void main(String[] args) throws Exception {
Philosopher[] philosophers = new Philosopher[5];
Object[] forks = new Object[philosophers.length];
for (int i = 0; i < forks.length; i++) {
forks[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object leftFork = forks[i];
Object rightFork = forks[(i + 1) % forks.length];
philosophers[i] = new Philosopher(leftFork, rightFork);
Thread t
= new Thread(philosophers[i], "Philosopher " + (i + 1));
t.start();
}
}
}
我们将每个分支建模为通用的Java对象,并制作尽可能多的哲学家。我们向每个哲学家传递他的左右叉子,他试图使用同步关键字锁定这些叉子。
运行此代码将生成类似于以下内容的输出。您的输出很可能与下面给出的输出不同,主要是因为sleep() 方法的调用间隔不同:
Philosopher 1 8038014601251: Thinking
Philosopher 2 8038014828862: Thinking
Philosopher 3 8038015066722: Thinking
Philosopher 4 8038015284511: Thinking
Philosopher 5 8038015468564: Thinking
Philosopher 1 8038016857288: Picked up left fork
Philosopher 1 8038022332758: Picked up right fork - eating
Philosopher 3 8038028886069: Picked up left fork
Philosopher 4 8038063952219: Picked up left fork
Philosopher 1 8038067505168: Put down right fork
Philosopher 2 8038089505264: Picked up left fork
Philosopher 1 8038089505264: Put down left fork. Back to thinking
Philosopher 5 8038111040317: Picked up left fork
所有的哲学家最初都是从思考开始的,我们看到哲学家1继续拿起左右叉子,然后吃东西,然后把它们都放下,之后“哲学家5”拿起它。
5. 解决方案的问题:死锁
尽管上述解决方案似乎是正确的,但出现了死锁的问题。
死锁是指系统进度停止的情况,因为每个进程都在等待获取由其他进程持有的资源。
我们可以通过运行上面的代码几次并检查某些时候代码只是挂起来确认相同的内容。下面是演示上述问题的示例输出:
Philosopher 1 8487540546530: Thinking
Philosopher 2 8487542012975: Thinking
Philosopher 3 8487543057508: Thinking
Philosopher 4 8487543318428: Thinking
Philosopher 5 8487544590144: Thinking
Philosopher 3 8487589069046: Picked up left fork
Philosopher 1 8487596641267: Picked up left fork
Philosopher 5 8487597646086: Picked up left fork
Philosopher 4 8487617680958: Picked up left fork
Philosopher 2 8487631148853: Picked up left fork
在这种情况下,每个哲学家都获得了他的左叉,但无法获得他的右叉,因为他的邻居已经获得了它。这种情况通常称为循环等待,是导致死锁并阻止系统进度的条件之一。
6. 解决死锁
正如我们在上面看到的,死锁的主要原因是循环等待条件,其中每个进程都等待由其他进程持有的资源。因此,为了避免死锁情况,我们需要确保循环等待条件被打破。有几种方法可以实现此目的,最简单的方法如下:
所有哲学家都先伸手去拿他们的左叉子,除了一个先伸手去拿右叉子的人。
我们通过对代码进行相对较小的更改来在现有代码中实现这一点:
public class DiningPhilosophers {
public static void main(String[] args) throws Exception {
final Philosopher[] philosophers = new Philosopher[5];
Object[] forks = new Object[philosophers.length];
for (int i = 0; i < forks.length; i++) {
forks[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object leftFork = forks[i];
Object rightFork = forks[(i + 1) % forks.length];
if (i == philosophers.length - 1) {
// The last philosopher picks up the right fork first
philosophers[i] = new Philosopher(rightFork, leftFork);
} else {
philosophers[i] = new Philosopher(leftFork, rightFork);
}
Thread t
= new Thread(philosophers[i], "Philosopher " + (i + 1));
t.start();
}
}
}
更改出现在上述代码的第 17-19 行,我们在其中引入了使最后一个哲学家首先到达他的右叉而不是左叉的条件。这打破了循环等待条件,我们可以避免死锁。
以下输出显示了所有哲学家都有机会思考和吃饭而不会造成僵局的情况之一:
Philosopher 1 88519839556188: Thinking
Philosopher 2 88519840186495: Thinking
Philosopher 3 88519840647695: Thinking
Philosopher 4 88519840870182: Thinking
Philosopher 5 88519840956443: Thinking
Philosopher 3 88519864404195: Picked up left fork
Philosopher 5 88519871990082: Picked up left fork
Philosopher 4 88519874059504: Picked up left fork
Philosopher 5 88519876989405: Picked up right fork - eating
Philosopher 2 88519935045524: Picked up left fork
Philosopher 5 88519951109805: Put down right fork
Philosopher 4 88519997119634: Picked up right fork - eating
Philosopher 5 88519997113229: Put down left fork. Back to thinking
Philosopher 5 88520011135846: Thinking
Philosopher 1 88520011129013: Picked up left fork
Philosopher 4 88520028194269: Put down right fork
Philosopher 4 88520057160194: Put down left fork. Back to thinking
Philosopher 3 88520067162257: Picked up right fork - eating
Philosopher 4 88520067158414: Thinking
Philosopher 3 88520160247801: Put down right fork
Philosopher 4 88520249049308: Picked up left fork
Philosopher 3 88520249119769: Put down left fork. Back to thinking
可以通过多次运行代码来验证系统是否摆脱了之前发生的死锁情况。
7. 结论
在本文中,我们探讨了著名的哲学家用餐问题以及循环等待和死锁的概念。我们编写了一个简单的解决方案,导致死锁,并进行了简单的更改以打破循环等待并避免死锁。这只是一个开始,更复杂的解决方案确实存在。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· Windows 提权-UAC 绕过