哲学家就餐问题(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
……
五、死锁预防策略
为了避免死锁,我们可以采用以下几种策略:
- 资源分配图算法:使用资源分配图算法(如银行家算法)来检测和避免死锁。该算法通过监控资源的分配和请求情况,确保系统始终处于安全状态,即不会发生死锁。
- 破坏死锁的四个必要条件:死锁的发生需要满足四个必要条件:互斥、占有并等待、不可剥夺和循环等待。我们可以通过破坏其中一个或多个条件来预防死锁。例如,在哲学家就餐问题中,我们可以通过改变资源分配方式来破坏循环等待条件,比如让一位哲学家先拿起右边的筷子,再拿起左边的筷子,这样就打破了循环等待的结构。
- 超时机制:在获取锁时设置超时时间。如果在指定时间内无法获取所有需要的资源,线程可以放弃已获取的资源,并重新尝试获取,从而避免无限期等待导致的死锁。在上述ReentrantLock的实现中,tryLock方法就是一种简单的超时机制体现,它尝试获取锁,如果在调用时锁不可用,会立即返回false,而不是一直阻塞等待。
文末总结
在本文中,我们详细探讨了Java中哲学家就餐问题的解决方案,包括使用同步块和ReentrantLock。同时,我们也讨论了死锁预防的策略。通过这些方法和策略,我们能够有效地处理多线程环境下的资源竞争和死锁问题,提高程序的并发性能和稳定性。