哲学家就餐问题是在计算机科学
中的一个经典问题,用来演示在并行计算
中多线程同步时产生的问题。这个问题被托尼·霍尔重新表述为哲学家就餐问题。这个问题可以用来解释死锁和资源耗尽。用来演示在并行计算中多线程同步时产生的问题,
就可以抽象成是资源抢占问题,而筷子就是“资源”。
哲学家就餐问题可以这样表述:假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一根筷子。因为用一根筷子很难吃到意大利面,所以哲学家必须用两根筷子吃东西,而且他们也只能使用自己左右手边的那两只筷子。
哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的筷子,永远都在等右边的筷子(或者相反)。即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只筷子超过五分钟后就放下自己手里的那一只筷子,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的筷子,那么这些哲学家就会等待五分钟,同时放下手中的筷子,再等五分钟,又同时拿起这些筷子。
先介绍一下死锁、活锁:
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。规范定义:集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的。
死锁发生的四个条件:
1、互斥条件:线程对资源的访问是排他性的,如果一个线程对占用了某资源,那么其他线程必须处于等待状态,直到资源被释放。
2、请求和保持条件:线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。
3、不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程剥夺,只能在使用完以后由自己释放。
4、环路等待条件:在死锁发生时,必然存在一个“进程-资源环形链”,即:{p0,p1,p2,...pn},进程p0(或线程)等待p1占用的资源,p1等待p2占用的资源,pn等待p0占用的资源。(最直观的理解是,p0等待p1占用的资源,而p1而在等待p0占用的资源,于是两个进程就相互等待)
产生死锁的原因主要是:
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。
活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。
关于“死锁与活锁”的比喻:
死锁:迎面开来的汽车A和汽车B,汽车A得到了半条路的资源(死锁发生条件1:资源访问是排他性的,A占了路你B不能过来,除非B从A头上飞过去),汽车B占了汽车A的另外半条路的资源,A想过去必须请求另一半被B占用的道路(死锁发生条件2:必须整条车身的空间才能开过去,A已经占了一半,但是另一半的路被B占用了),B若想过去也必须等待A让路,但是因为一些原因两方都没有让路(死锁发生条件3:在未使用完资源前,不能被其他线程剥夺),于是两者相互僵持一个都走不了(死锁发生条件4:环路等待条件),而且导致整条道上的后续车辆也走不了。
活锁:马路中间有条小桥,只能容纳一辆车经过,桥两头开来两辆车A和B,A比较礼貌,示意B先过,B也比较礼貌,示意A先过,结果两人一直谦让谁也过不去。
在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源。一种常用的计算机技术是资源加锁,用来保证在某个时刻,资源只能被一个程序或一段代码访问。当一个程序想要使用的资源已经被另一个程序锁定,它就等待资源解锁。当多个程序涉及到加锁的资源时,在某些情况下就有可能发生死锁。例如,某个程序需要访问两个文件,当两个这样的程序各锁了一个文件,那它们都在等待对方解锁另一个文件,而这永远不会发生。
问题解法
1.服务生解法
一个简单的解法是引入一个餐厅服务生,哲学家必须经过他的允许才能拿起餐叉。因为服务生知道哪只餐叉正在使用,所以他能够作出判断避免死锁。
为了演示这种解法,假设哲学家依次标号为A至E。如果A和C在吃东西,则有四只餐叉在使用中。B坐在A和C之间,所以两只餐叉都无法使用,而D和E之间有一只空余的餐叉。假设这时D想要吃东西。如果他拿起了第五只餐叉,就有可能发生死锁。相反,如果他征求服务生同意,服务生会让他等待。这样,我们就能保证下次当两把餐叉空余出来时,一定有一位哲学家可以成功的得到一对餐叉,从而避免了死锁。
2.资源分级解法
另一个简单的解法是为资源(这里是餐叉)分配一个偏序或者分级的关系,并约定所有资源都按照这种顺序获取,按相反顺序释放,而且保证不会有两个无关资源同时被同一项工作所需要。在哲学家就餐问题中,资源(餐叉)按照某种规则编号为1至5,每一个工作单元(哲学家)总是先拿起左右两边编号较低的餐叉,再拿编号较高的。用完餐叉后,他总是先放下编号较高的餐叉,再放下编号较低的。在这种情况下,当四位哲学家同时拿起他们手边编号较低的餐叉时,只有编号最高的餐叉留在桌上,从而第五位哲学家就不能使用任何一只餐叉了。而且,只有一位哲学家能使用最高编号的餐叉,所以他能使用两只餐叉用餐。当他吃完后,他会先放下编号最高的餐叉,再放下编号较低的餐叉,从而让另一位哲学家拿起后边的这只开始吃东西。
尽管资源分级能避免死锁,但这种策略并不总是实用的,特别是当所需资源的列表并不是事先知道的时候。例如,假设一个工作单元拿着资源3和5,并决定需要资源2,则必须先要释放5,之后释放3,才能得到2,之后必须重新按顺序获取3和5。对需要访问大量数据库记录的计算机程序来说,如果需要先释放高编号的记录才能访问新的记录,那么运行效率就不会高,因此这种方法在这里并不实用。
这种方法经常是实际计算机科学问题中最实用的解法,通过为分级锁指定常量,强制获得锁的顺序,就可以解决这个问题。
3.Chandy/Misra解法
1984年,K. Mani Chandy和J. Misra提出了哲学家就餐问题的另一个解法,允许任意的用户(编号P1, ..., Pn)争用任意数量的资源。与迪科斯彻的解法不同的是,这里编号可以是任意。完全的分布式的方法,不需要仲裁者。
以下是一些情况下遇到的判断:
(1) 首先,给哲学家编号。
(2) 若两个哲学家竞争同一个筷子的时候,把筷子给编号比较小的人。
(3) 筷子有两个状态,净的和脏的。初始状态下,所有的筷子都是脏的。
(4) 哲学家要吃饭时必须拿起两只筷子,当他缺乏某只筷子的时候,发起请求。
(5) 若拿着筷子的哲学家受到了请求,如果筷子是干净的,则他不放下筷子;若是脏的,则放下筷子,并且在放下之前,把筷子擦干净。
(6) 哲学家吃完的时候,筷子变脏,当他有邻座请求筷子的时候,擦干净筷子,然后给邻座。
根据之上的判断出现了以下的流程:
- 起初,先把 5 只筷子分别分给 A~E 五位哲学家,并定义为脏的。
- 假设B想要吃东西了,他把手里的2号筷子擦干净,可是还缺1号筷子呀,于是就向拥有1号筷子的A发送一个请求。
- A收到请求,因为此时1号筷子是脏的,所以A得把1号筷子擦干净,并交给B。
- B有两只筷子了,准备开吃了。可就在此时,C也想吃东西了,向B发送过来一个请求,想要2号筷子。
- B手里的2号筷子是干净的呀,于是B先吃自己的,吃完以后2号筷子变成脏的了,B再把筷子擦干净并交给C。
..........
.....
这个解法允许很大的并行性,适用于任意大的问题。
代码实例:
package
Philosopher.ChandyMisra;
import
java.util.Random;
import
java.util.concurrent.Executor;
import
java.util.concurrent.ExecutorService;
import
java.util.concurrent.Executors;class Stick {
int
owner;
boolean
dirty;
int
number;
public
synchronized
void
setStatus(
boolean
status){ dirty = status; }
public
synchronized
boolean
getStatus(){
return
this
.dirty; }
public
synchronized
void
setOwner(
int
owner){
this
.owner = owner; }
public
synchronized
int
getOwner(){
return
owner; }
public
Stick(
boolean
dirty,
int
number) {
this
.dirty = dirty;
this
.number = number; }}
public
class
Philo
implements
Runnable
{
Stick leftStick, rightStick;
Philo leftPhilo, rightPhilo;
int
number, eatTimes;
public
Philo(
int
number,
int
eatTimes) {
this
.number = number;
this
.eatTimes = eatTimes; }
public
void
eating(){
try
{ Thread.sleep(
3000
); }
catch
(InterruptedException ie){ System.out.println(
"Catch an Interrupted Exception!"
);
return
; } leftStick.setStatus(
true
); rightStick.setStatus(
true
); eatTimes++; System.out.println(
"Philo "
+ number +
"is eating "
+
": "
+ eatTimes );
//dinningExam.repaint();
}
public
boolean
answer(Stick used){
boolean
retFlag =
false
;
synchronized
(
this
){
if
(used.getStatus()){
if
(used == leftStick) used.setOwner(leftPhilo.number);
else
if
(used == rightStick) used.setOwner(rightPhilo.number);
else
{ System.out.println(
"Error status!"
); retFlag =
false
; } used.setStatus(
false
); retFlag =
true
; }
else
retFlag =
false
; }
if
(retFlag) System.out.println(
"Philo "
+ number +
"request success!"
);
//dinningExam.repaint();
return
retFlag; }
@Override
public
void
run() { Random r =
new
Random();
int
intR;
while
(
true
){
while
(leftStick.getOwner() != number | rightStick.getOwner() != number){ intR = r.nextInt();
if
(intR %
2
==
0
&& leftStick.getOwner() != number) leftPhilo.answer(leftStick);
else
if
(intR %
2
==
1
&& rightStick.getOwner() != number) rightPhilo.answer(rightStick); }
synchronized
(
this
){
if
(leftStick.getOwner() == number && rightStick.getOwner() == number){ eating(); } }
try
{
int
sleepSec = r.nextInt();
if
(sleepSec <
0
) sleepSec = -sleepSec; Thread.sleep(sleepSec %
500
); }
catch
(InterruptedException ie){ System.out.println(
"Catch an Interrupted Exception!"
); } } }
public
static
void
main(String[] args) { ExecutorService exec = Executors.newCachedThreadPool(); Philo[] ps =
new
Philo[
5
]; Stick[] ss =
new
Stick[
5
];
for
(
int
i =
0
; i <
5
; i++) { ss[i] =
new
Stick(
true
, i); ps[i]=
new
Philo(i,
0
); } System.out.println((
0
-
1
)%
5
);
for
(
int
i =
0
; i <
5
; i++) { ps[i].leftPhilo= ps[(i+
4
)%
5
]; ps[i].rightPhilo= ps[(i+
1
)%
5
]; ps[i].leftStick = ss[i]; ps[i].rightStick=ss[(i+
1
)%
5
]; }
for
(
int
i =
0
; i <
5
; i++) { exec.execute(ps[i]); } exec.shutdown(); }}
结果根据哲学家的需求和当时的情况进行分配而不是人为的结果。