经典的哲学家就餐问题,还有印象吗?

开发 前端
在本文中,我们详细探讨了Java中哲学家就餐问题的解决方案,包括使用同步块和ReentrantLock。同时,我们也讨论了死锁预防的策略。通过这些方法和策略,我们能够有效地处理多线程环境下的资源竞争和死锁问题,提高程序的并发性能和稳定性。​

哲学家就餐问题(Dining Philosophers Problem)是由计算机科学家艾兹格・W・迪科斯彻(Edsger W. Dijkstra)于 1965 年提出的一个经典的多线程同步问题,用于描述和解决并发编程中的资源分配与死锁问题。

在本文中,我们探讨如何使用Java解决这一问题。

一、问题描述

想象有一张圆桌,周围坐着五位哲学家。每位哲学家的面前都有一碗食物(如意大利面)和两根筷子(分别位于其左右两侧)。

哲学家们的生活只有两种状态:思考和吃饭。要吃饭的话,哲学家必须同时拿起他左边和右边的筷子,吃完后放下筷子继续思考。

由于筷子数量有限,只有五根,且每位哲学家都需要两根筷子才能进餐,这就可能导致资源竞争和死锁的情况发生。

例如,每个哲学家都先拿起了左边的筷子,然后等待右边的筷子,此时所有哲学家都在等待其他哲学家放下筷子,而又都不会放下自己已经拿到的筷子,这样就造成了死锁,整个系统陷入僵持状态,没有哲学家能够吃到食物。

二、解决办法

为了解决这个问题,需要设计一种合理的资源分配策略,确保哲学家们能够有序地获取和释放筷子,避免死锁的发生。常见的解决方案包括:

  • 资源分级:给筷子编号,规定哲学家先拿起编号小的筷子,再拿起编号大的筷子,这样可以避免循环等待。
  • 奇数哲学家先拿左边筷子,偶数哲学家先拿右边筷子:打破循环等待的条件,使得至少有一位哲学家能够获取到两根筷子开始进餐,从而打破死锁局面。
  • 使用并发控制机制:如信号量、互斥锁等,来控制哲学家对筷子的访问,确保资源的合理分配和同步。

三、使用同步块解决哲学家就餐问题

解决哲学家就餐问题的一种方法是使用Java的同步块。

我们可以为每根筷子创建一个对象,将其作为锁。哲学家线程在尝试拿起筷子时会获取相应的锁。

以下是实现代码:

public class DiningPhilosophersSynchronized {
    public static void main(String[] args) {
        Object[] chopsticks = new Object[5];
        for (int i = 0; i < 5; i++) {
            chopsticks[i] = new Object();
        }

        Thread[] philosophers = new Thread[5];
        for (int i = 0; i < 5; i++) {
            int leftChopstick = i;
            int rightChopstick = (i + 1) % 5;
            philosophers[i] = new Thread(new Philosopher(chopsticks[leftChopstick], chopsticks[rightChopstick]));
            philosophers[i].setName("Philosopher " + (i + 1));
            philosophers[i].start();
        }
    }

    static class Philosopher implements Runnable {
        private final Object leftChopstick;
        private final Object rightChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + " is thinking");
                    Thread.sleep((long) (Math.random() * 1000));

                    System.out.println(Thread.currentThread().getName() + " is hungry");

                    synchronized (leftChopstick) {
                        System.out.println(Thread.currentThread().getName() + " picked up left chopstick");
                        synchronized (rightChopstick) {
                            System.out.println(Thread.currentThread().getName() + " picked up right chopstick");
                            System.out.println(Thread.currentThread().getName() + " is eating");
                            Thread.sleep((long) (Math.random() * 1000));
                            System.out.println(Thread.currentThread().getName() + " put down right chopstick");
                        }
                        System.out.println(Thread.currentThread().getName() + " put down left chopstick");
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

在上述代码中:

  • 我们创建了一个包含五个对象的数组chopsticks,每个对象代表一根筷子。
  • 为每个哲学家创建一个线程。每个哲学家线程尝试获取左边和右边筷子的锁,以模拟拿起筷子吃饭的过程。
  • 在Philosopher类的run方法中,哲学家首先思考一段时间,然后进入饥饿状态,尝试获取两根筷子。如果成功获取两根筷子,就开始吃饭,吃完后放下筷子继续思考。

运行结果为:

Philosopher 3 is thinking
Philosopher 5 is thinking
Philosopher 4 is thinking
Philosopher 1 is thinking
Philosopher 2 is thinking
Philosopher 4 is hungry
Philosopher 4 picked up left chopstick
Philosopher 4 picked up right chopstick
Philosopher 4 is eating
Philosopher 3 is hungry
Philosopher 3 picked up left chopstick
Philosopher 4 put down right chopstick
Philosopher 4 put down left chopstick
Philosopher 3 picked up right chopstick
Philosopher 3 is eating
Philosopher 4 is thinking
Philosopher 1 is hungry
Philosopher 1 picked up left chopstick
……

四、使用 ReentrantLock 解决哲学家就餐问题

除了同步块,我们还可以使用ReentrantLock来解决哲学家就餐问题。ReentrantLock提供了比同步块更灵活的锁机制,例如可中断的锁获取、公平性选择等。

以下是使用ReentrantLock的实现:

import java.util.concurrent.locks.ReentrantLock;

public class DiningPhilosophersReentrantLock {
    public static void main(String[] args) {
        ReentrantLock[] chopsticks = new ReentrantLock[5];
        for (int i = 0; i < 5; i++) {
            chopsticks[i] = new ReentrantLock();
        }

        Thread[] philosophers = new Thread[5];
        for (int i = 0; i < 5; i++) {
            int leftChopstick = i;
            int rightChopstick = (i + 1) % 5;
            philosophers[i] = new Thread(new Philosopher(chopsticks[leftChopstick], chopsticks[rightChopstick]));
            philosophers[i].setName("Philosopher " + (i + 1));
            philosophers[i].start();
        }
    }

    static class Philosopher implements Runnable {
        private final ReentrantLock leftChopstick;
        private final ReentrantLock rightChopstick;

        public Philosopher(ReentrantLock leftChopstick, ReentrantLock rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + " is thinking");
                    Thread.sleep((long) (Math.random() * 1000));

                    System.out.println(Thread.currentThread().getName() + " is hungry");

                    boolean leftAcquired = false;
                    boolean rightAcquired = false;
                    try {
                        leftAcquired = leftChopstick.tryLock();
                        if (leftAcquired) {
                            System.out.println(Thread.currentThread().getName() + " picked up left chopstick");
                            rightAcquired = rightChopstick.tryLock();
                            if (rightAcquired) {
                                System.out.println(Thread.currentThread().getName() + " picked up right chopstick");
                                System.out.println(Thread.currentThread().getName() + " is eating");
                                Thread.sleep((long) (Math.random() * 1000));
                            }
                        }
                    } finally {
                        if (rightAcquired) {
                            rightChopstick.unlock();
                            System.out.println(Thread.currentThread().getName() + " put down right chopstick");
                        }
                        if (leftAcquired) {
                            leftChopstick.unlock();
                            System.out.println(Thread.currentThread().getName() + " put down left chopstick");
                        }
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

在这个实现中:

  • 我们创建了一个ReentrantLock数组来表示筷子。
  • 在Philosopher类的run方法中,哲学家尝试使用tryLock方法获取左边和右边筷子的锁。tryLock方法会尝试获取锁,如果锁不可用,它会立即返回false,而不会阻塞线程。
  • 如果成功获取两根筷子,哲学家开始吃饭,吃完后释放锁。这种方式可以避免死锁,因为如果无法获取两根筷子,哲学家会释放已经获取的筷子,从而避免一直持有锁导致其他哲学家无法获取资源。

运行结果为:

Philosopher 1 is thinking
Philosopher 3 is thinking
Philosopher 4 is thinking
Philosopher 2 is thinking
Philosopher 5 is thinking
Philosopher 4 is hungry
Philosopher 4 picked up left chopstick
Philosopher 4 picked up right chopstick
Philosopher 4 is eating
Philosopher 2 is hungry
Philosopher 2 picked up left chopstick
Philosopher 2 picked up right chopstick
Philosopher 2 is eating
Philosopher 5 is hungry
Philosopher 5 is thinking
Philosopher 4 put down right chopstick
……

五、死锁预防策略

为了避免死锁,我们可以采用以下几种策略:

  1. 资源分配图算法:使用资源分配图算法(如银行家算法)来检测和避免死锁。该算法通过监控资源的分配和请求情况,确保系统始终处于安全状态,即不会发生死锁。
  2. 破坏死锁的四个必要条件:死锁的发生需要满足四个必要条件:互斥、占有并等待、不可剥夺和循环等待。我们可以通过破坏其中一个或多个条件来预防死锁。例如,在哲学家就餐问题中,我们可以通过改变资源分配方式来破坏循环等待条件,比如让一位哲学家先拿起右边的筷子,再拿起左边的筷子,这样就打破了循环等待的结构。
  3. 超时机制:在获取锁时设置超时时间。如果在指定时间内无法获取所有需要的资源,线程可以放弃已获取的资源,并重新尝试获取,从而避免无限期等待导致的死锁。在上述ReentrantLock的实现中,tryLock方法就是一种简单的超时机制体现,它尝试获取锁,如果在调用时锁不可用,会立即返回false,而不是一直阻塞等待。

文末总结

在本文中,我们详细探讨了Java中哲学家就餐问题的解决方案,包括使用同步块和ReentrantLock。同时,我们也讨论了死锁预防的策略。通过这些方法和策略,我们能够有效地处理多线程环境下的资源竞争和死锁问题,提高程序的并发性能和稳定性。

责任编辑:武晓燕 来源: 看山的小屋
相关推荐

2025-01-07 09:11:39

同步问题磁带驱动器

2013-08-30 09:54:18

2015-10-10 10:51:25

数据本质大数据

2022-07-29 14:22:11

AI

2016-09-23 15:51:49

2023-04-25 14:00:00

GPTAI

2015-11-18 17:46:37

软件工程

2021-07-21 16:56:33

人工智能机器学习技术

2020-06-09 18:52:04

机器学习技术人工智能

2022-01-17 21:29:36

通信信息电线

2023-05-26 15:36:56

2010-04-23 12:27:10

华为

2024-04-30 15:06:03

智能体模型工具

2020-05-06 19:47:15

人工智能AI

2024-05-15 07:26:50

RedisBigKey优化

2010-08-25 16:26:59

研发

2009-10-09 14:43:00

CCNA的未来CCNA

2015-10-08 09:50:12

谷歌作恶

2009-11-02 10:51:46

2014-01-09 09:24:32

Linux渗透测试渗透测试
点赞
收藏

51CTO技术栈公众号