问题描述
哲学家就餐问题(Dining philosophers problem)是在计算机科学中的一个经典问题,用来演示在并发计算中多线程同步时产生的问题。
在1971年,著名的计算机科学家艾兹格·迪科斯彻提出了一个同步问题,即假设有五台计算机都试图访问五份共享的磁带驱动器。稍后,这个问题被托尼·霍尔重新表述为哲学家就餐问题。这个问题可以用来解释死锁和资源耗尽。
哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。
哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。
即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生活锁。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。
在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源。一种常用的计算机技术是资源加锁,用来保证在某个时刻,资源只能被一个程序或一段代码访问。当一个程序想要使用的资源已经被另一个程序锁定,它就等待资源解锁。当多个程序涉及到加锁的资源时,在某些情况下就有可能发生死锁。例如,某个程序需要访问两个文件,当两个这样的程序各锁了一个文件,那它们都在等待对方解锁另一个文件,而这永远不会发生。
死锁的必要条件
死锁的产生具备以下四个条件:
- 互斥条件:指线程对己经获取到的资源进行排它性使用, 即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
- 请求并持有条件: 指一个线程己经持有了至少一个资源 , 但又提出了新的资源请求 ,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源。
- 不可剥夺条件: 指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
- 环路等待条件:指在发生死锁时,必然存在一个线程→资源的环形链,即线程集合{ T0,T1,T2 ,…,Tn }中的 T0 正在等待一个 T1 占用的资源,T1 正在等待 T2 占用的资源,……Tn 正在等待己被 T0 占用的资源。
复现死锁
当所有哲学家同时决定进餐,拿起左边筷子时候,就发生了死锁。
public class Problem {
public static void main(String[] args) {
int sum = 5;
Philosopher[] philosophers = new Philosopher[sum];
Chopstick[] chopsticks = new Chopstick[sum];
for (int i = 0; i < sum; i++) {
chopsticks[i] = new Chopstick();
}
for (int i = 0; i < sum; i++) {
Chopstick left = chopsticks[i];
Chopstick right = chopsticks[(i + 1) % sum];
philosophers[i] = new Philosopher(left, right);
new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
}
}
}
class Chopstick {
}
class Philosopher implements Runnable {
private final Chopstick left;
private final Chopstick right;
public Philosopher(Chopstick left, Chopstick right) {
this.left = left;
this.right = right;
}
@Override
public void run() {
try {
while (true) {
doAction("思考");
synchronized (left) {
doAction("拿起左边筷子");
synchronized (right) {
doAction("拿起右边筷子--------开吃了");
doAction("吃完了,放下筷子");
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 10));
}
}
解决方法
资源分级算法(破坏死锁的环路等待条件)
资源分级算法是指为资源分配一个偏序或者分级的关系,并约定所有资源都按照这种顺序获取,按相反顺序释放。对应在哲学家就餐问题中就是为各个餐叉设置 1 - 2 - 3 - 4 - 5 的序号,每一个哲学家总是先拿起左右两边编号较低的餐叉,再拿编号较高的。用完餐叉后,他总是先放下编号较高的餐叉,再放下编号较低的。在这种情况下,1 ~ 4 号哲学家都是左边的餐叉序号小,而 5 号哲学家是右边的餐叉序号小,当 1 ~ 4 号哲学家同时拿起他们手边编号较低的餐叉即 1~4 号餐叉时,只有编号最高的 5 号餐叉留在桌上,5 号哲学家先申请序号较小的 1 号,发现已经被拿走,所以他就只能等待。而剩下的那支 5 号餐叉被 4 号哲学家成功获得。当 4 号哲学家吃完后,他会先放下编号最高的餐叉,再放下编号较低的餐叉,从而使得 3 号哲学家成功获得他所需的第二支餐叉,以此类推,整个系统不会发生死锁。实际执行顺序还是要看 CPU 的分配,不过这样已经不会构成循环了。
此处给筷子添加 id,根据 id 从小到大获取(不用关心编号的具体规则,只要保证编号是全局唯一并且有序的)。
代码如下:
public class Solution1 {
public static void main(String[] args) {
int sum = 5;
Philosopher[] philosophers = new Philosopher[sum];
Chopstick[] chopsticks = new Chopstick[sum];
for (int i = 0; i < sum; i++) {
chopsticks[i] = new Chopstick(i);
}
for (int i = 0; i < sum; i++) {
Chopstick left = chopsticks[i];
Chopstick right = chopsticks[(i + 1) % sum];
philosophers[i] = new Philosopher(left, right);
new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
}
}
}
class Chopstick {
private int id;
public Chopstick(int id) {
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
class Philosopher implements Runnable {
private final Chopstick left;
private final Chopstick right;
public Philosopher(Chopstick left, Chopstick right) {
if (left.getId() < right.getId()) {
this.left = left;
this.right = right;
} else {
this.left = right;
this.right = left;
}
}
@Override
public void run() {
try {
while (true) {
doAction("思考");
synchronized (left) {
doAction("拿起左边筷子");
synchronized (right) {
doAction("拿起右边筷子--------开吃了");
doAction("吃完了,放下筷子");
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 10));
}
}
破坏死锁的请求并持有条件
1、使用多把锁,每把锁使用 tryLock
为获取锁操作设置超时时间。
代码如下:
public class Solution2 {
public static void main(String[] args) {
int sum = 5;
Philosopher[] philosophers = new Philosopher[sum];
ReentrantLock[] chopsticks = new ReentrantLock[sum];
for (int i = 0; i < sum; i++) {
chopsticks[i] = new ReentrantLock();
}
for (int i = 0; i < sum; i++) {
ReentrantLock left = chopsticks[i];
ReentrantLock right = chopsticks[(i + 1) % sum];
philosophers[i] = new Philosopher(left, right);
new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
}
}
}
class Philosopher implements Runnable {
private final ReentrantLock left;
private final ReentrantLock right;
public Philosopher(ReentrantLock left, ReentrantLock right) {
this.left = left;
this.right = right;
}
@Override
public void run() {
try {
while (true) {
doAction("思考");
left.lock();
try {
doAction("拿起左边筷子");
if (right.tryLock(10, TimeUnit.MILLISECONDS)) {
try {
doAction("拿起右边筷子--------开吃了");
} finally {
right.unlock();
doAction("吃完了,放下筷子");
}
} else {
// 没有获取到右手的筷子,放弃并继续思考
}
} finally {
left.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 10));
}
}
2、使用一把锁,设置条件队列 Condition
。
该方法只用一把锁,没有 Chopstick
类,将竞争从对筷子的争夺转换成了对状态的判断。仅当左右邻座都没有进餐时才可以进餐。
public class Solution3 {
public static void main(String[] args) {
int sum = 5;
Philosopher[] philosophers = new Philosopher[sum];
ReentrantLock lock = new ReentrantLock();
// 安排哲学家就坐
for (int i = 0; i < sum; i++) {
philosophers[i] = new Philosopher(lock);
}
// 设置哲学家的左右邻居
for (int i = 0; i < sum; i++) {
Philosopher left = philosophers[(i + sum) % sum];
Philosopher right = philosophers[(i + 1) % sum];
philosophers[i].setLeft(left);
philosophers[i].setRight(right);
new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
}
}
}
class Philosopher implements Runnable {
private boolean eating;
private Philosopher left;
private Philosopher right;
private final ReentrantLock lock;
private final Condition condition;
public Philosopher(ReentrantLock lock) {
eating = false;
this.lock = lock;
this.condition = lock.newCondition();
}
public void setLeft(Philosopher left) {
this.left = left;
}
public void setRight(Philosopher right) {
this.right = right;
}
public void think() throws InterruptedException {
lock.lock();
try {
eating = false;
System.out.println(Thread.currentThread().getName() + "开始思考");
left.condition.signal();
right.condition.signal();
} finally {
lock.unlock();
}
Thread.sleep(10);
}
public void eat() throws InterruptedException {
lock.lock();
try {
// 左右两边只要有任意哲学家在吃饭,就等待
while (left.eating || right.eating) {
condition.await();
}
System.out.println(Thread.currentThread().getName() + "开始吃饭");
eating = true;
} finally {
lock.unlock();
}
Thread.sleep(10);
}
@Override
public void run() {
try {
while (true) {
think();
eat();
}
} catch (InterruptedException e) {
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 10));
}
}
参考文章https://www.jianshu.com/p/99f10708b1e1
更多解法可以参考leetcode