一、问题描述
哲学家进餐问题是由 Dijkstra 提出并解决的,该问题是描述有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。如下图所示:
二、代码模拟实现
经分析可知,放在桌子上的筷子是临界资源,在一段时间内只允许一位哲学家使用。为了实现对筷子的互斥使用,可以用一个信号量表示一只筷子,由这五个信号量构成信号量数组。其java代码实现如下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
/**
* @author: hh
* @date: 2021/1/12 20:31
* @description: 哲学家进餐,模拟哲学家同时拿起左手边筷子
*/
public class PhilosopherMeal extends Thread {
/**
* 哲学家名称前缀
*/
private static String NAME_PREFIX = "哲学家";
/**
* 哲学家编号
*/
private int philosopherNum;
/**
* 模拟筷子资源信号量
*/
private volatile List<Semaphore> semaphores;
public PhilosopherMeal(int philosopherNum,List<Semaphore> semaphores){
super(PhilosopherMeal.NAME_PREFIX+String.valueOf(philosopherNum));
this.philosopherNum = philosopherNum;
this.semaphores = semaphores;
}
@Override
public void run() {
try {
//哲学家先拿起左手边筷子
semaphores.get(this.philosopherNum).acquire();
//哲学家拿起右手边筷子
semaphores.get(this.getRightSemaphoreIndex(this.philosopherNum)).acquire();
System.out.println(this.getName()+"获取到了筷子"+String.valueOf(this.philosopherNum)+"和"+ String.valueOf(this.getRightSemaphoreIndex(this.philosopherNum)) + "开始吃饭了");
} catch (Exception e) {
e.printStackTrace();
}
finally {
semaphores.get(this.philosopherNum).release();
semaphores.get(this.getRightSemaphoreIndex(this.philosopherNum)).release();
}
}
/**
* 获取当前哲学家右手边的筷子编号
*
* @param philosopherNum :当前哲学家编号
* @return :右手边筷子编号
*/
private int getRightSemaphoreIndex(int philosopherNum){
return (philosopherNum - 1 < 0) ? semaphores.size() - 1 : philosopherNum - 1;
}
public static void main(String[] args){
List<Semaphore> semaphores = new ArrayList<>();
int counter = 5;
for(int i = 0 ; i < counter ; i++){
semaphores.add(new Semaphore(1));
}
for(int i = 0 ; i < counter ; i++){
new PhilosopherMeal(i,semaphores).start();
}
}
}
运行结果如下图:
在以上描述中,当哲学家饥饿时,总是先去拿他左边的筷子,即执行 semaphores.get(this.philosopherNum).acquire();成功后,再去拿他右边的筷子,即执行 semaphores.get(this.getRightSemaphoreIndex(this.philosopherNum)).acquire();又成功后便可进餐。进餐完毕,又先放下他左边的筷子,然后再放右边的筷子。虽然,上述解法可保证不会有两个相邻的哲学家同时进餐,但有可能引起死锁。假如五位哲学家同时饥饿而各自拿起左边的筷子时,就会使五个信号量均为 0; 当他们再试图去拿右边的筷子时,都将因无筷子可拿而无限期地等待。对于这样的死锁问题。如下代码模拟死锁在代码中加入一行Thread.sleep(1000); 即模拟五个哲学家同时拿到左手边筷子,代码如下所示:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
/**
* @author: hh
* @date: 2021/1/12 20:31
* @description: 哲学家进餐,模拟哲学家同时拿起左手边筷子,使其产生死锁
*/
public class PhilosopherMeal extends Thread {
/**
* 哲学家名称前缀
*/
private static String NAME_PREFIX = "哲学家";
/**
* 哲学家编号
*/
private int philosopherNum;
/**
* 模拟筷子资源信号量
*/
private volatile List<Semaphore> semaphores;
public PhilosopherMeal(int philosopherNum,List<Semaphore> semaphores){
super(PhilosopherMeal.NAME_PREFIX+String.valueOf(philosopherNum));
this.philosopherNum = philosopherNum;
this.semaphores = semaphores;
}
@Override
public void run() {
try {
//哲学家先拿起左手边筷子
semaphores.get(this.philosopherNum).acquire();
//睡眠1s,使其它哲学家有时间拿左手边的筷子
Thread.sleep(1000);
//哲学家拿起右手边筷子
semaphores.get(this.getRightSemaphoreIndex(this.philosopherNum)).acquire();
System.out.println(this.getName()+"获取到了筷子"+String.valueOf(this.philosopherNum)+"和"+ String.valueOf(this.getRightSemaphoreIndex(this.philosopherNum)) + "开始吃饭了");
} catch (Exception e) {
e.printStackTrace();
}
finally {
semaphores.get(this.philosopherNum).release();
semaphores.get(this.getRightSemaphoreIndex(this.philosopherNum)).release();
}
}
/**
* 获取当前哲学家右手边的筷子编号
*
* @param philosopherNum :当前哲学家编号
* @return :右手边筷子编号
*/
private int getRightSemaphoreIndex(int philosopherNum){
return (philosopherNum - 1 < 0) ? semaphores.size() - 1 : philosopherNum - 1;
}
public static void main(String[] args){
List<Semaphore> semaphores = new ArrayList<>();
int counter = 5;
for(int i = 0 ; i < counter ; i++){
semaphores.add(new Semaphore(1));
}
for(int i = 0 ; i < counter ; i++){
new PhilosopherMeal(i,semaphores).start();
}
}
}
三、解决死锁问题
可采取以下几种解决方法:
1. 至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用毕时能释放出他用过的两只筷子,从而使更多的哲学家能够进餐。
2. 仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐。
3. 规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子,而偶数号哲学家则相反。按此规定,将是 1、2 号哲学家竞争 1 号筷子;3、4 号哲学家竞争 3 号筷子。即五位哲学家都先竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一位哲学家能获得两只筷子而进餐。
本文只实现2、3两种方法,第1个方法读者可参考代码自行实现,方案2代码如下所示:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author: hh
* @date: 2021/1/12 20:31
* @description: 哲学家进餐解决方法1:规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子,而偶数号哲学家则
* 相反。按此规定,将是 0、1 号哲学家竞争 0号筷子;2、3 号哲学家竞争 2 号筷子。即五位
* 哲学家都先竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一位哲学家能获
* 得两只筷子而进餐。
*/
public class PhilosopherMealSolution1 extends Thread {
/**
* 哲学家名称前缀
*/
private final static String NAME_PREFIX = "哲学家";
/**
* 哲学家编号
*/
private final int philosopherNum;
/**
* 模拟筷子资源信号量
*/
private volatile ArrayList<Semaphore> semaphores;
public PhilosopherMealSolution1(int philosopherNum,ArrayList<Semaphore> semaphores){
super(PhilosopherMealSolution1.NAME_PREFIX+String.valueOf(philosopherNum));
this.philosopherNum = philosopherNum;
this.semaphores = semaphores;
}
@Override
public void run() {
try {
if(this.philosopherNum % 2 == 0){
//哲学家先拿起左手边筷子
semaphores.get(this.philosopherNum).acquire();
//睡眠1s,使其它哲学家有时间拿左手边的筷子
Thread.sleep(1000);
//哲学家拿起右手边筷子
semaphores.get(this.getRightSemaphoreIndex(this.philosopherNum)).acquire();
}else{
//哲学家拿起右手边筷子
semaphores.get(this.getRightSemaphoreIndex(this.philosopherNum)).acquire();
//睡眠1s,使其它哲学家有时间拿左手边的筷子
Thread.sleep(1000);
//哲学家先拿起左手边筷子
semaphores.get(this.philosopherNum).acquire();
}
System.out.println(this.getName()+"获取到了筷子"+String.valueOf(this.philosopherNum)+"和"+ String.valueOf(this.getRightSemaphoreIndex(this.philosopherNum)) + "开始吃饭了");
} catch (Exception e) {
e.printStackTrace();
}
finally {
semaphores.get(this.philosopherNum).release();
semaphores.get(this.getRightSemaphoreIndex(this.philosopherNum)).release();
}
}
/**
* 获取当前哲学家右手边的筷子编号
*
* @param philosopherNum :当前哲学家编号
* @return :右手边筷子编号
*/
private int getRightSemaphoreIndex(int philosopherNum){
return (philosopherNum - 1 < 0) ? semaphores.size() - 1 : philosopherNum - 1;
}
public static void main(String[] args){
ArrayList<Semaphore> semaphores = new ArrayList<>();
int counter = 5;
for(int i = 0 ; i < counter ; i++){
semaphores.add(new Semaphore(1));
}
for(int i = 0 ; i < counter ; i++){
new PhilosopherMealSolution1(i,semaphores).start();
}
}
}
方案3代码如下所示:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author: hh
* @date: 2021/1/12 20:31
* @description: 哲学家进餐解决方法2:仅当哲学家的左、右两只筷子均可用时,才允许他拿起筷子进餐
*/
public class PhilosopherMealSolution2 extends Thread {
/**
* 哲学家名称前缀
*/
private final static String NAME_PREFIX = "哲学家";
/**
* 用于控制and 信号量访问的锁
*/
private ReentrantLock semaphoreLock;
/**
* 哲学家编号
*/
private final int philosopherNum;
/**
* 模拟筷子资源信号量
*/
private volatile List<Semaphore> semaphores;
public PhilosopherMealSolution2(int philosopherNum, List<Semaphore> semaphores,ReentrantLock semaphoreLock){
super(PhilosopherMealSolution2.NAME_PREFIX+String.valueOf(philosopherNum));
this.philosopherNum = philosopherNum;
this.semaphores = semaphores;
this.semaphoreLock = semaphoreLock;
}
@Override
public void run() {
try {
//只有同时申请到左手边和右手边的筷子才能继续向下执行
this.await(semaphores.get(this.philosopherNum),semaphores.get(this.getRightSemaphoreIndex(this.philosopherNum)));
Thread.sleep(1000);
System.out.println(this.getName()+"获取到了筷子"+String.valueOf(this.philosopherNum)+"和"+ String.valueOf(this.getRightSemaphoreIndex(this.philosopherNum)) + "开始吃饭了");
} catch (Exception e) {
e.printStackTrace();
}
finally {
this.release(semaphores.get(this.philosopherNum),semaphores.get(this.getRightSemaphoreIndex(this.philosopherNum)));
}
}
/**
* 获取当前哲学家右手边的筷子编号
*
* @param philosopherNum :当前哲学家编号
* @return :右手边筷子编号
*/
private int getRightSemaphoreIndex(int philosopherNum){
return (philosopherNum - 1 < 0) ? semaphores.size() - 1 : philosopherNum - 1;
}
/**
*and 信号量,只有数组中的信号量都是同时可获取的,才可以继续往下走,否则则阻塞
*
* @param semaphores :信号量数组
*/
private void await(Semaphore ... semaphores){
semaphoreLock.lock();
try{
for(Semaphore semaphore : semaphores){
semaphore.acquire();
}
}catch (Exception e){
e.printStackTrace();
}finally {
semaphoreLock.unlock();
}
}
/**
* 同时释放多个信号量
*
* @param semaphores : 信号量数组
*/
private void release(Semaphore ... semaphores){
try{
for(Semaphore semaphore : semaphores){
semaphore.release();
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args){
List<Semaphore> semaphores = new ArrayList<>();
ReentrantLock semaphoreLock = new ReentrantLock();
int counter = 5;
for(int i = 0 ; i < counter ; i++){
semaphores.add(new Semaphore(1));
}
for(int i = 0 ; i < counter ; i++){
new PhilosopherMealSolution2(i,semaphores,semaphoreLock).start();
}
}
}