由於線程可能進入堵塞狀態,而且由於對象可能擁有“同步”方法——除非同步鎖定被解除,否則線程不能訪問那個對象——所以一個線程完全可能等候另一個對象,而另一個對象又在等候下一個對象,以此類推。這個“等候”鏈最可怕的情形就是進入封閉狀態——最後那個對象等候的是第一個對象!此時,所有線程都會陷入無休止的相互等待狀態,大家都動彈不得。我們將這種情況稱為“死鎖”。盡管這種情況並非經常出現,但一旦碰到,程序的調試將變得異常艱難。
就語言本身來說,尚未直接提供防止死鎖的幫助措施,需要我們通過謹慎的設計來避免。如果有誰需要調試一個死鎖的程序,他是沒有任何竅門可用的。
1. Java 1.2對stop(),suspend(),resume()以及destroy()的反對
為減少出現死鎖的可能,Java 1.2作出的一項貢獻是“反對”使用Thread的stop(),suspend(),resume()以及destroy()方法。
之所以反對使用stop(),是因為它不安全。它會解除由線程獲取的所有鎖定,而且如果對象處於一種不連貫狀態(“被破壞”),那麼其他線程能在那種狀態下檢查和修改它們。結果便造成了一種微妙的局面,我們很難檢查出真正的問題所在。所以應盡量避免使用stop(),應該采用Blocking.java那樣的方法,用一個標志告訴線程什麼時候通過退出自己的run()方法來中止自己的執行。
如果一個線程被堵塞,比如在它等候輸入的時候,那麼一般都不能象在Blocking.java中那樣輪詢一個標志。但在這些情況下,我們仍然不該使用stop(),而應換用由Thread提供的interrupt()方法,以便中止並退出堵塞的代碼。
//: Interrupt.java
// The alternative approach to using stop()
// when a thread is blocked
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
class Blocked extends Thread {
public synchronized void run() {
try {
wait(); // Blocks
} catch(InterruptedException e) {
System.out.println("InterruptedException");
}
System.out.println("Exiting run()");
}
}
public class Interrupt extends Applet {
private Button
interrupt = new Button("Interrupt");
private Blocked blocked = new Blocked();
public void init() {
add(interrupt);
interrupt.addActionListener(
new ActionListener() {
public
void actionPerformed(ActionEvent e) {
System.out.println("Button pressed");
if(blocked == null) return;
Thread remove = blocked;
blocked = null; // to release it
remove.interrupt();
}
});
blocked.start();
}
public static void main(String[] args) {
Interrupt applet = new Interrupt();
Frame aFrame = new Frame("Interrupt");
aFrame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
aFrame.add(applet, BorderLayout.CENTER);
aFrame.setSize(200,100);
applet.init();
applet.start();
aFrame.setVisible(true);
}
} ///:~
Blocked.run()內部的wait()會產生堵塞的線程。當我們按下按鈕以後,blocked(堵塞)的句柄就會設為null,使垃圾收集器能夠將其清除,然後調用對象的interrupt()方法。如果是首次按下按鈕,我們會看到線程正常退出。但在沒有可供“殺死”的線程以後,看到的便只是按鈕被按下而已。
suspend()和resume()方法天生容易發生死鎖。調用suspend()的時候,目標線程會停下來,但卻仍然持有在這之前獲得的鎖定。此時,其他任何線程都不能訪問鎖定的資源,除非被“掛起”的線程恢復運行。對任何線程來說,如果它們想恢復目標線程,同時又試圖使用任何一個鎖定的資源,就會造成令人難堪的死鎖。所以我們不應該使用suspend()和resume(),而應在自己的Thread類中置入一個標志,指出線程應該活動還是掛起。若標志指出線程應該掛起,便用wait()命其進入等待狀態。若標志指出線程應當恢復,則用一個notify()重新啟動線程。我們可以修改前面的Counter2.java來實際體驗一番。盡管兩個版本的效果是差不多的,但大家會注意到代碼的組織結構發生了很大的變化——為所有“聽眾”都使用了匿名的內部類,而且Thread是一個內部類。這使得程序的編寫稍微方便一些,因為它取消了Counter2.java中一些額外的記錄工作。
//: Suspend.java
// The alternative approach to using suspend()
// and resume(), which have been deprecated
// in Java 1.2.
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
public class Suspend extends Applet {
private TextField t = new TextField(10);
private Button
suspend = new Button("Suspend"),
resume = new Button("Resume");
class Suspendable extends Thread {
private int count = 0;
private boolean suspended = false;
public Suspendable() { start(); }
public void fauxSuspend() {
suspended = true;
}
public synchronized void fauxResume() {
suspended = false;
notify();
}
public void run() {
while (true) {
try {
sleep(100);
synchronized(this) {
while(suspended)
wait();
}
} catch (InterruptedException e){}
t.setText(Integer.toString(count++));
}
}
}
private Suspendable ss = new Suspendable();
public void init() {
add(t);
suspend.addActionListener(
new ActionListener() {
public
void actionPerformed(ActionEvent e) {
ss.fauxSuspend();
}
});
add(suspend);
resume.addActionListener(
new ActionListener() {
public
void actionPerformed(ActionEvent e) {
ss.fauxResume();
}
});
add(resume);
}
public static void main(String[] args) {
Suspend applet = new Suspend();
Frame aFrame = new Frame("Suspend");
aFrame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e){
System.exit(0);
}
});
aFrame.add(applet, BorderLayout.CENTER);
aFrame.setSize(300,100);
applet.init();
applet.start();
aFrame.setVisible(true);
}
} ///:~
Suspendable中的suspended(已掛起)標志用於開關“掛起”或者“暫停”狀態。為掛起一個線程,只需調用fauxSuspend()將標志設為true(真)即可。對標志狀態的偵測是在run()內進行的。就象本章早些時候提到的那樣,wait()必須設為“同步”(synchronized),使其能夠使用對象鎖。在fauxResume()中,suspended標志被設為false(假),並調用notify()——由於這會在一個“同步”從句中喚醒wait(),所以fauxResume()方法也必須同步,使其能在調用notify()之前取得對象鎖(這樣一來,對象鎖可由要喚醍的那個wait()使用)。如果遵照本程序展示的樣式,可以避免使用wait()和notify()。
Thread的destroy()方法根本沒有實現;它類似一個根本不能恢復的suspend(),所以會發生與suspend()一樣的死鎖問題。然而,這一方法沒有得到明確的“反對”,也許會在Java以後的版本(1.2版以後)實現,用於一些可以承受死鎖危險的特殊場合。
大家可能會奇怪當初為什麼要實現這些現在又被“反對”的方法。之所以會出現這種情況,大概是由於Sun公司主要讓技術人員來決定對語言的改動,而不是那些市場銷售人員。通常,技術人員比搞銷售的更能理解語言的實質。當初犯下了錯誤以後,也能較為理智地正視它們。這意味著Java能夠繼續進步,即便這使Java程序員多少感到有些不便。就我自己來說,寧願面對這些不便之處,也不願看到語言停滯不前。