死锁
某个任务在等待另一个任务,而后者又等待别的任务,这样一直下去,直到这个链条上的任务又在等待第一个任务释放锁。这得到了一个“任务之间互相等待的连续循环”,没有哪个线程能继续。这被称之为死锁。
如果你运行一个程序,而他马上就死锁了,你可以立即跟踪下去。真正的问题在于,程序可能看起来工作良好,但是具有潜在的死锁危险。这时,死锁可能发生,而事先却没有任何征兆,所以缺陷会潜伏在你的程序里,直到客户端发现它出乎意料地发生(以一种几乎肯定是很难重现的方式发生)。因此,在编写并发程序的时候,进行仔细的程序设计以防止死锁是关键部分。
案例
由Edsger Dijkstra提出的哲学家就餐问题是一个经典的死锁例证。该问题的基本描述是指定五个哲学家(不过这里的例子将允许任意数目)。这些哲学家将花部分时间思考,花部分时间就餐。当他们思考的时候,不需要任何共享资源;但当他们就餐时,将使用有限数量的餐具。在问题的原始描述中,餐具是叉子。要吃到桌子中央盘子里的意大利面条需要两把叉子,不过把餐具看成是筷子更合理;很明显,哲学家就餐就需要两根筷子。
问题中引入的难点是:作为哲学家,他们很穷,所以他们只能买五根筷子(更一般地讲,筷子和哲学家的数量相同)。他们围坐在桌子周围,每人之间放一根筷子。当一个哲学家要就餐的时候,这个哲学家必须同时得到左边和右边的筷子。如果一个哲学家左边或右边已经有人在使用筷子了,那么这个哲学家就必须等待,直至可得到必须的筷子。
案例代码:
这里的测试哲学家用2个。更容易发生死锁。
/**
* 筷子
*
*/
public class Chopstick {
/** 取得 */
private boolean taken = false;
public synchronized void take() throws InterruptedException {
while (taken)
wait();
taken = true;
}
/** 放下筷子 */
public synchronized void drop() {
taken = false;
notifyAll();
}
}
任何两个Philosopher都不能成功take()同一根筷子。另外,如果一根Chopstick已经被某个Philosopher获得,那么另一个Philosopher可以wait(),直至这根Chopstick的当前持有者调用drop()使其可用为止。
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* 哲学家
*
*/
public class Philosopher implements Runnable {
private Chopstick left;
private Chopstick right;
private final int id;
private final int ponderFactor;
private Random rand = new Random(47);
/** 暂停,犹豫 */
private void pause() throws InterruptedException {
if (ponderFactor == 0)
return;
TimeUnit.MICROSECONDS.sleep(rand.nextInt(ponderFactor * 250));
}
public Philosopher(Chopstick left, Chopstick right, int id, int ponder) {
this.left = left;
this.right = right;
this.id = id;
this.ponderFactor = ponder;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
System.out.println(this + " thinking");
pause();
// Philosopher becomes hungry
System.out.println(this + " grabbing right begin");
right.take();
System.out.println(this + " grabbing right success");
System.out.println(this + " grabbing left begin");
left.take();
System.out.println(this + " grabbing left success");
System.out.println(this + " eating");
pause();
right.drop();
left.drop();
}
} catch (InterruptedException e) {
System.out.println(this + " exiting via interrupt");
}
}
public String toString() {
return "Philosopher " + id;
}
}
在Philosopher,run()中,每个Philosopher只是不断地思考和吃饭。如果PonderFactor不为0,则pause()方法会休眠(sleeps())一段随机的时间。通过这种方式,你将看到Philosopher会在思考上花掉一段随机化的时间,然后尝试着获取(take())右边和左边的Chopstick,随后在吃饭上再花掉一段随机化的时间,之后重复此过程。
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 死锁 哲学家进餐
*
*/
public class DeadlockingDiningPhilosophers {
public static void main(String[] args) throws IOException, InterruptedException {
int ponder = 0;
int size = 2;
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++) {
if (i != size - 1)
exec.execute(new Philosopher(sticks[i], sticks[i + 1], i, ponder));
else
exec.execute(new Philosopher(sticks[i], sticks[0], i, ponder));
}
System.out.println("Press 'Enter' to quit");
System.in.read();
exec.shutdownNow();
}
}
如果Philosopher花在思考上的时间非常少,那么当他们需要进餐时,全都会在Chopstick上产生竞争,而死锁也就会更快地发生。如果有许多Philosopher,或者他们花费很多时间思考,那么尽管存在死锁的可能,但你可能永远也看不到死锁,值为0的命令行参数倾向于使得死锁尽快发生。此处取ponde取0,size取2.
运行结果(其中一个)
示意图:
从输出中可以看到, Philosopher 0 首先 take 了他右边的 Chopstick1,然后Philosopher 0 和 Philosopher 1 竞争 take Chopstick0,Philosopher 1占了上风 take 了 Chopstick 0,Philosopher 0 只能 wait 。至此两个哲学家各自占有自己右边的筷子,且又同时等待对方已经 take 的筷子,所以产生了死锁,不能再继续运行下去。
分析
要修正死锁的问题,就必须明白,当以下四个条件同时满足时,就会发生死锁:
1)互斥条件。任务使用的资源至少有一个是不能共享的。这里,一根Chopstic一次只能被一个Philosopher使用。
2)至少有一个任务它必须持有一个资源且正在等待获取一个被当前别的任务持有的资源。也就是说,要发生死锁,Philosopher必须拿着一根Chopstick并且等待另一根。
3)资源不能被任务抢占,任务必须把资源释放当做普通事件。Philosopher很有礼貌,他们不会从其他Philosopher那里抢Chopstick。
4)必须有循环等待,这时,一个任务等待其他任务任务所持有的资源,后者又在等待另一个任务所持有的资源,这样一直下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住。在DeadlockingDiningPhilosophers.java中,因为每个Philosopher都试图先得到右边的Chopstick,然后得到左边的Chopstic,所以发生了循环等待。
因为要发生死锁的话,所有这些条件必须全部满足,所以要防止死锁的话,只需要破坏其中一个即可。在程序中,防止死锁最容易的方法是破坏第四个条件。
有这个条件的原因是每个Philosopher都试图用特定的顺序那Chopstick:先右后左。正因为如此,就可能会发生“每个人都拿着右边的Chopstick,并且等待左边的Chopstick”的情况,这就是循环等待情况。然后,如果最后一个Philosopher被初始化成先拿左边的Chopstick,后拿右边的Chopstick,那么这个Philosopher将永远不会阻止其右边的Philosopher拿起他们的Chopstick。本例中,这就可以防止循环等待。这只是问题的解决方法之一,也可以通过破坏其他条件来防止死锁。
/**
* 通过改变四个造成死锁条件中的第四个循环等待来移除死锁。
*
*/
public class FixedDiningPhilosophers {
public static void main(String[] args) throws InterruptedException, IOException {
int ponder = 0;
int size = 2;
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++)
if (i < size-1)
exec.execute(new Philosopher(sticks[i], sticks[i + 1], i, ponder));
else
// 修改最后一个的拿筷子顺序
exec.execute(new Philosopher(sticks[0], sticks[i], i, ponder));
System.out.println("Press 'Enter' to quit");
System.in.read();
exec.shutdownNow();
}
}
不停输出,不会死锁。
参考《Java编程思想》