由于线程能被阻塞,更由于synchronized方法能阻止其它线程访问本对象,因此有可能会出现如下这种情况:线程一在等线程二(释放某个对象),线程二又在等线程三,这样依次排下去直到有个线程在等线程一。这样就形成了一个环,每个线程都在等对方释放资源,而它们谁都不能运行。这就是所谓的死锁(deadlock)。
如果程序一运行就死锁,那倒也简单了。你可以马上着手解决这个问题。但真正的麻烦在于,程序看上去能正常运行,但是却潜伏着会引起死锁的隐患。或许你认为这里根本就不可能会有死锁,而bug也就这样潜伏下来了。直到有一天,让某个用户给撞上了(而且这种bug还很可能是不可重复的)。所以对并发编程来说,防止死锁是设计阶段的一个重要任务。
下面我们来看看由Dijkstra发现的经典的死锁场景:哲学家吃饭问题。原版的故事里有五个哲学家(不过我们的例程里允许有任意数量)。这些哲学家们只做两件事,思考和吃饭。他们思考的时候,不需要任何共享资源,但是吃饭的时候,就必须坐到餐桌旁。餐桌上的餐具是有限的。原版的故事里,餐具是叉子,吃饭的时候要用两把叉子把面条从碗里捞出来。但是很明显,把叉子换成筷子会更合理,所以:一个哲学家需要两根筷子才能吃饭。
现在引入问题的关键:这些哲学家很穷,只买得起五根筷子。他们坐成一圈,两个人的中间放一根筷子。哲学家吃饭的时候必须同时得到左手边和右手边的筷子。如果他身边的任何一位正在使用筷子,那他只有等着。
这个问题之所以有趣就在于,它演示了这么一个程序,它看上去似乎能正常运行,但是却容易引起死锁。你可以自己试试,用命令行参数调节哲学家的数量和思考的时间。如果有很多哲学家,而且/或者他们思考的时间很长,或许你永远也碰不到死锁,但是死锁的可能性总还是在的。默认的命令行参数会让它很快地死锁:
<strong><span style="font-family:KaiTi_GB2312;font-size:18px;"><strong>//: c13:DiningPhilosophers.java
// Demonstrates how deadlock can be hidden in a program.
// {Args: 5 0 deadlock 4}
import java.util.*;
class Chopstick {
private static int counter = 0;
private int number = counter++;
public String toString() {
return "Chopstick " + number;
}
}
class Philosopher extends Thread {
private static Random rand = new Random();
private static int counter = 0;
private int number = counter++;
private Chopstick leftChopstick;
private Chopstick rightChopstick;
static int ponder = 0; // Package access
public Philosopher(Chopstick left, Chopstick right) {
leftChopstick = left;
rightChopstick = right;
start();
}
public void think() {
System.out.println(this + " thinking");
if(ponder > 0)
try {
sleep(rand.nextInt(ponder));
} catch(InterruptedException e) {
throw new RuntimeException(e);
}
}
public void eat() {
synchronized(leftChopstick) {
System.out.println(this + " has "
+ this.leftChopstick + " Waiting for "
+ this.rightChopstick);
synchronized(rightChopstick) {
System.out.println(this + " eating");
}
}
}
public String toString() {
return "Philosopher " + number;
}
public void run() {
while(true) {
think();
eat();
}
}
}
public class DiningPhilosophers {
public static void main(String[] args) {
if(args.length < 3) {
System.err.println("usage:\n" +
"java DiningPhilosophers numberOfPhilosophers " +
"ponderFactor deadlock timeout\n" +
"A nonzero ponderFactor will generate a random " +
"sleep time during think().\n" +
"If deadlock is not the string " +
"'deadlock', the program will not deadlock.\n" +
"A nonzero timeout will stop the program after " +
"that number of seconds.");
System.exit(1);
}
Philosopher[] philosopher =
new Philosopher[Integer.parseInt(args[0])];
Philosopher.ponder = Integer.parseInt(args[1]);
Chopstick
left = new Chopstick(),
right = new Chopstick(),
first = left;
int i = 0;
while(i < philosopher.length - 1) {
philosopher[i++] =
new Philosopher(left, right);
left = right;
right = new Chopstick();
}
if(args[2].equals("deadlock"))
philosopher[i] = new Philosopher(left, first);
else // Swapping values prevents deadlock:
philosopher[i] = new Philosopher(first, left);
// Optionally break out of program:
if(args.length >= 4) {
int delay = Integer.parseInt(args[3]);
if(delay != 0)
new Timeout(delay * 1000, "Timed out");
}
}
} ///:~
</strong></span></strong>
Chopstick和Philosopher都包含一个能自动递增的static counter。每个Philosopher有两个reference,一个表示左边那根Chopstick,另一个表示右边那根;Philosopher吃饭之前必须拿到这两根筷子。
static的ponder表示哲学家要花多长时间思考。如果这个值非零,则think( )会用它来生成一个随机的休眠时间。我们用这种方法证明,如果线程(也就是哲学家)花在其它事情上(思考)的时间多了,那么它们使用共享资源(筷子)的机会就少了,因而程序就不太容易死锁了,但事实并非如此。请看eat( )。Philosopher先synchronized左边那根筷子。如果得不到,他就等,这时他处于阻塞状态。得到左边那根筷子之后,他又用相同的方法去申请右边的筷子。吃完之后,他也是先放左边的,再放右边的。Philosopher在run()里不停的思考和吃饭。
最近看马士兵的j2se中,讲到死锁那里的时候,马士兵老师讲到了五个哲学家吃饭谁都不肯舍弃自己的筷子,最终饿死的事情,其实就是一个死锁的问题,就是你不能访问别人,别人也不能访问你,你把自己锁在了自己的一个小圈子里。可以看出锁有好处,就是一旦上锁,在锁释放之前,没有其他人可以访问,从而保证了安全,但是生活中的我们不能为自己的生活加锁,看似保护了自己,实际上是与他人的距离在逐渐加大,保护私有物品,我们可以上锁,但是生活中的我们还是要向外开放的。