由於線程可能進入堵塞狀態,而且由於對象可能擁有“同步”方法——除非同步鎖定被解除,否則線程不能訪問那個對象——所以一個線程完全可能等候另一個對象,而另一個對象又在等候下一個對象,以此類推。這個“等候”鏈最可怕的情形就是進入封閉狀態——最後那個對象等候的是第一個對象!此時,所有線程都會陷入無休止的相互等待狀態,大家都動彈不得。我們將這種情況稱為“死鎖”。盡管這種情況並非經常出現,但一旦碰到,程序的調試將變得異常艱難。
就語言本身來說,尚未直接提供防止死鎖的幫助措施,需要我們通過謹慎的設計來避免。如果有誰需要調試一個死鎖的程序,他是沒有任何竅門可用的。
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程序員多少感到有些不便。就我自己來說,寧願面對這些不便之處,也不願看到語言停滯不前。