死锁
通过前几篇的介绍,我们知道一个对象可以用synchronized方法或其他形式的加锁机制来防止别的任务在互斥还锁没有释放的时候就访问这个对象。我们还知道,任务可以变成阻塞状态(wait或是await的阻塞方式,即释放锁),所以就可能出现这种情况:某个任务在等待另一个任务,而后者有等待别的任务,这样一直下去,直到链条上的任务有在等待第一个任务释放锁。这就得到了一个任务之间互相等待的连环套,而没有哪个线程可以继续,这就是死锁。
哲学家就餐问题就是一个经典的死锁问题:这些哲学家将花部分时间思考(等待),花部分时间进餐(抢筷子)。他们思考的时候,不需要任何共享资源,但就餐时,将使用有限数量的餐具——哲学家和筷子的数量相同,但是一个哲学家需要两根筷子才能吃饭,这个哲学家必须同时得到左边和右边的筷子。如果一个哲学家左边或者右边已经有人在使用筷子了,那么这个哲学家就必须等待,知道左右两根筷子同时可用:
class Chopstick {
//某个哲学家(任务)调用take()方法,表示该哲学家(任务)希望得到这根筷子,如果taken属性为true,表示这根筷子正在被别人使用,直到持有这根筷子的哲学家释放筷子(taken设为false),这根筷子将被这个哲学家争夺,若争夺上,则设为true。
public synchronized void take() throws InterruptedException {
while(taken) {
wait();
}
taken = true;
}
//某个哲学家调用drop()方法,表示将释放该资源,并通知其他哲学家(任务)可以争夺该资源
pulic synchronized void drop() {
taken = false;
notifyAll();
}
}
任何两个哲学家都不能take()同一根筷子。另外,如果一个筷子已经被某个哲学家获得,那么另一哲学家就必须wait(),直到这根Chopsticks的当前持有者调用drop()可用为止。
当一个哲学家调用take()时,他将等待,直到这根筷子的taken为false(资源可用)。然后这个哲学家将占用这根筷子(taken设为true),当这个哲学家使用完这根筷子时,它会调用drop()来修改标志的状态,并notifyAll()所有其他的哲学家,这些哲学家就有可能在wait()这根筷子。
//哲学家类
public class Philosopher implements Runnable {
private Chopstick left;
private Chopstick right;
private final int id;
private final ponderFactor;
private Random random = new Random(47);
//该方法表示哲学家在思考,如果ponderFactor 不为0,将随机休眠一定时间
private void pause() throws InterruptedException {
if(ponderFactor == 0) {
return;
}
TimeUnit.MILLSECONDS.sleep(random.nextInt(ponderFactor * 250));
}
public Philosopher(Chopstick left,Chopstick right, int ident,int ponder) {
this.left = left;
this.right = right;
id = ident;
ponderFactor = ponder;
}
public void run() {
try {
while(!Thread.interrpted()) {
System.out.print(this + " " + "thinking");
//哲学家在思考
pause();
//哲学家试图获取左手的筷子,如果该筷子的taken属性为true,表示该筷子正在被其他哲学家使用,这个哲学家将等待筷子被释放
left.take();
//等待该筷子的taken属性被设置为false,该科学家有机会抢到该筷子
System.out.print(this + " " + "grabing right");
//哲学家试图获取右手的筷子,如果该筷子的taken属性为true,表示该筷子正在被其他哲学家使用,这个哲学家将等待筷子被释放
right.take();
//等待该筷子的taken属性被设置为false,该科学家有机会抢到该筷子
System.out.print(this + " " + "grabing left");
System.out.print(this + " " + "eating");
//哲学家在思考
pause();
//哲学家释放右手的筷子,taken被设为false,同时通知其他哲学家可以抢夺该筷子
right.drop();
//哲学家释放左手的筷子,taken被设为false,同时通知其他哲学家可以抢夺该筷子
left.drop();
}
} catch(InterruptedException e) {
System.out.print(this+ " " + "eating via interrupt");
}
}
public String toString() {
return "Philosopher " + id;
}
}
下面是可能会造成死锁的代码:
public class DeadlockingDiningPhilosopher {
public static void main(String[] args) {
public static void main(String[] args) {
int ponder = 5;
if(args.length > 0) {
ponder = Integer.parseInt(args[0]);
}
int size = 5;
if(args.length > 1) {
size = Integer.parseInt(args[1]);
}
ExecutorService exec = Executors.newCachedThreadPool();
Chopstick[] sticks = new Chopstick[size];
for(int i = 0;i < size; ++i) {
sticks[i] = new Chopstick();
}
for(int i = 0;i < size; ++i) {
exec.execute(new Philosopher(sticks[i], stick[(i + 1) % size], i , ponder));
}
if(args.length == 3 && args[2].equals("timeout")) {
TimeUnit.SECONDS.sleep(5);
} else {
System.out.print("Press 'Enter' to quit");
System.in.read();
}
exec.shutdownNow();
}
}
}
有这样一种情况:当第一个哲学家执行left.take()的时候,发现这根筷子的taken为false,表示这根筷子没有被占用,于是将这根左手的筷子据为己有,当他准备执行right.take()的时候(准备拿右手的边筷子的时候),该任务(第一个哲学家)挂起,第二个哲学开始吃饭,他调用了left.take(),发现这根筷子没有没占用(第一个哲学家右手的筷子,同时也是第二个哲学家左手的筷子),于是把taken置为true。这时第一个哲学家继续准备拿到右手的筷子(准备执行right.take()),但是他发现taken为true,这时它会调用wait()阻塞住,直到持有这个筷子的哲学家调用notify(),即第二个哲学家调用left.drop(),但是很不幸,当第二个哲学家准备调用right.take()的时候,即他打算拿起右手的筷子时,第二个哲学家被挂起了,第三个哲学家拿到了左手的筷子(第二个哲学家希望右手拿到),于是再执行第二个哲学家的任务时,他阻塞了,需要等待第三个哲学家的左手释放筷子。。。以此类推,第五个哲学家左手拿到了筷子,还需要拿到右手的筷子,但是他发现拿不了,因为这根快筷子已经被第一个哲学家的左手拿到了,而他的右手在等待第二个哲学家左手的筷子。。。这就是死锁,一个连环套。
以下是死锁产生的4个条件,这四个条件必须同时满足,才会发生死锁,只要有一个条件不满足,就一定不会死锁:
互斥条件:任务使用的资源中至少有一个是不能共享的。这里,一根筷子一次只能被一个哲学家使用。
至少有一个任务必须持有一个资源且正在等待获取一个当前被别的任务持有的资源。这里,哲学家必须拿着一根筷子而等待另一根。
资源不能被任务抢占:哲学家很有礼貌,不会从其他哲学家那里抢筷子。
必须持有循环等待。
为了防止发生死锁,只需要破坏其中一个条件就行。这里,第4个是最容易破坏的条件:因为每个哲学家都试图用特定的顺序拿筷子先左后右,这就出现了之前描述的问题,即所有人左手都持有筷子而等待右手能得到筷子。那么解决方式是破坏其中一个顺序,如,让第5个哲学家拿着右手的筷子等待得到左手的筷子:
public class FixedDining {
public static void main(String[] args) {
... ...
... ...
for(int i = 0; i < size; ++i) {
sticks[i] = new Chopstick();
}
for(int i = 0; i < size; ++i) {
if(i < size - 1) {
//除了最后一个哲学家以外,其余哲学家都是顺势拿筷子。
exec.execute(new Philosopher(stick[i], stick[i + 1], i ,ponder));
} else {
//关键的一句,让最后一个哲学家的左手拿第一根筷子,右手拿最后一根筷子,即逆势拿筷子。
exec.execute(new Philosopher(stick[0],stick[i], i ,ponder));
}
... ...
... ...
}
}
}