堵塞狀態是前述四種狀態中最有趣的,值得我們作進一步的探討。線程被堵塞可能是由下述五方面的原因造成的:
(1) 調用sleep(毫秒數),使線程進入“睡眠”狀態。在規定的時間內,這個線程是不會運行的。
(2) 用suspend()暫停了線程的執行。除非線程收到resume()消息,否則不會返回“可運行”狀態。
(3) 用wait()暫停了線程的執行。除非線程收到nofify()或者notifyAll()消息,否則不會變成“可運行”(是的,這看起來同原因2非常相象,但有一個明顯的區別是我們馬上要揭示的)。
(4) 線程正在等候一些IO(輸入輸出)操作完成。
(5) 線程試圖調用另一個對象的“同步”方法,但那個對象處於鎖定狀態,暫時無法使用。
亦可調用yield()(Thread類的一個方法)自動放棄CPU,以便其他線程能夠運行。然而,假如調度機制覺得我們的線程已擁有足夠的時間,並跳轉到另一個線程,就會發生同樣的事情。也就是說,沒有什麼能防止調度機制重新啟動我們的線程。線程被堵塞後,便有一些原因造成它不能繼續運行。
下面這個例子展示了進入堵塞狀態的全部五種途徑。它們全都存在於名為Blocking.java的一個文件中,但在這兒采用散落的片斷進行解釋(大家可注意到片斷前後的“Continued”以及“Continuing”標志。利用第17章介紹的工具,可將這些片斷連結到一起)。首先讓我們看看基本的框架:
//: Blocking.java // Demonstrates the various ways a thread // can be blocked. import java.awt.*; import java.awt.event.*; import java.applet.*; import java.io.*; //////////// The basic framework /////////// class Blockable extends Thread { private Peeker peeker; protected TextField state = new TextField(40); protected int i; public Blockable(Container c) { c.add(state); peeker = new Peeker(this, c); } public synchronized int read() { return i; } protected synchronized void update() { state.setText(getClass().getName() + " state: i = " + i); } public void stopPeeker() { // peeker.stop(); Deprecated in Java 1.2 peeker.terminate(); // The preferred approach } } class Peeker extends Thread { private Blockable b; private int session; private TextField status = new TextField(40); private boolean stop = false; public Peeker(Blockable b, Container c) { c.add(status); this.b = b; start(); } public void terminate() { stop = true; } public void run() { while (!stop) { status.setText(b.getClass().getName() + " Peeker " + (++session) + "; value = " + b.read()); try { sleep(100); } catch (InterruptedException e){} } } } ///:Continued
Blockable類打算成為本例所有類的一個基礎類。一個Blockable對象包含了一個名為state的TextField(文本字段),用於顯示出對象有關的信息。用於顯示這些信息的方法叫作update()。我們發現它用getClass.getName()來產生類名,而不是僅僅把它打印出來;這是由於update(0不知道自己為其調用的那個類的准確名字,因為那個類是從Blockable衍生出來的。
在Blockable中,變動指示符是一個int i;衍生類的run()方法會為其增值。
針對每個Bloackable對象,都會啟動Peeker類的一個線程。Peeker的任務是調用read()方法,檢查與自己關聯的Blockable對象,看看i是否發生了變化,最後用它的status文本字段報告檢查結果。注意read()和update()都是同步的,要求對象的鎖定能自由解除,這一點非常重要。
1. 睡眠
這個程序的第一項測試是用sleep()作出的:
///:Continuing ///////////// Blocking via sleep() /////////// class Sleeper1 extends Blockable { public Sleeper1(Container c) { super(c); } public synchronized void run() { while(true) { i++; update(); try { sleep(1000); } catch (InterruptedException e){} } } } class Sleeper2 extends Blockable { public Sleeper2(Container c) { super(c); } public void run() { while(true) { change(); try { sleep(1000); } catch (InterruptedException e){} } } public synchronized void change() { i++; update(); } } ///:Continued
在Sleeper1中,整個run()方法都是同步的。我們可看到與這個對象關聯在一起的Peeker可以正常運行,直到我們啟動線程為止,隨後Peeker便會完全停止。這正是“堵塞”的一種形式:因為Sleeper1.run()是同步的,而且一旦線程啟動,它就肯定在run()內部,方法永遠不會放棄對象鎖定,造成Peeker線程的堵塞。
Sleeper2通過設置不同步的運行,提供了一種解決方案。只有change()方法才是同步的,所以盡管run()位於sleep()內部,Peeker仍然能訪問自己需要的同步方法——read()。在這裡,我們可看到在啟動了Sleeper2線程以後,Peeker會持續運行下去。
2. 暫停和恢復
這個例子接下來的一部分引入了“掛起”或者“暫停”(Suspend)的概述。Thread類提供了一個名為suspend()的方法,可臨時中止線程;以及一個名為resume()的方法,用於從暫停處開始恢復線程的執行。顯然,我們可以推斷出resume()是由暫停線程外部的某個線程調用的。在這種情況下,需要用到一個名為Resumer(恢復器)的獨立類。演示暫停/恢復過程的每個類都有一個相關的恢復器。如下所示:
///:Continuing /////////// Blocking via suspend() /////////// class SuspendResume extends Blockable { public SuspendResume(Container c) { super(c); new Resumer(this); } } class SuspendResume1 extends SuspendResume { public SuspendResume1(Container c) { super(c);} public synchronized void run() { while(true) { i++; update(); suspend(); // Deprecated in Java 1.2 } } } class SuspendResume2 extends SuspendResume { public SuspendResume2(Container c) { super(c);} public void run() { while(true) { change(); suspend(); // Deprecated in Java 1.2 } } public synchronized void change() { i++; update(); } } class Resumer extends Thread { private SuspendResume sr; public Resumer(SuspendResume sr) { this.sr = sr; start(); } public void run() { while(true) { try { sleep(1000); } catch (InterruptedException e){} sr.resume(); // Deprecated in Java 1.2 } } } ///:Continued
SuspendResume1也提供了一個同步的run()方法。同樣地,當我們啟動這個線程以後,就會發現與它關聯的Peeker進入“堵塞”狀態,等候對象鎖被釋放,但那永遠不會發生。和往常一樣,這個問題在SuspendResume2裡得到了解決,它並不同步整個run()方法,而是采用了一個單獨的同步change()方法。
對於Java 1.2,大家應注意suspend()和resume()已獲得強烈反對,因為suspend()包含了對象鎖,所以極易出現“死鎖”現象。換言之,很容易就會看到許多被鎖住的對象在傻乎乎地等待對方。這會造成整個應用程序的“凝固”。盡管在一些老程序中還能看到它們的蹤跡,但在你寫自己的程序時,無論如何都應避免。本章稍後就會講述正確的方案是什麼。
3. 等待和通知
通過前兩個例子的實踐,我們知道無論sleep()還是suspend()都不會在自己被調用的時候解除鎖定。需要用到對象鎖時,請務必注意這個問題。在另一方面,wait()方法在被調用時卻會解除鎖定,這意味著可在執行wait()期間調用線程對象中的其他同步方法。但在接著的兩個類中,我們看到run()方法都是“同步”的。在wait()期間,Peeker仍然擁有對同步方法的完全訪問權限。這是由於wait()在掛起內部調用的方法時,會解除對象的鎖定。
我們也可以看到wait()的兩種形式。第一種形式采用一個以毫秒為單位的參數,它具有與sleep()中相同的含義:暫停這一段規定時間。區別在於在wait()中,對象鎖已被解除,而且能夠自由地退出wait(),因為一個notify()可強行使時間流逝。
第二種形式不采用任何參數,這意味著wait()會持續執行,直到notify()介入為止。而且在一段時間以後,不會自行中止。
wait()和notify()比較特別的一個地方是這兩個方法都屬於基礎類Object的一部分,不象sleep(),suspend()以及resume()那樣屬於Thread的一部分。盡管這表面看有點兒奇怪——居然讓專門進行線程處理的東西成為通用基礎類的一部分——但仔細想想又會釋然,因為它們操縱的對象鎖也屬於每個對象的一部分。因此,我們可將一個wait()置入任何同步方法內部,無論在那個類裡是否准備進行涉及線程的處理。事實上,我們能調用wait()的唯一地方是在一個同步的方法或代碼塊內部。若在一個不同步的方法內調用wait()或者notify(),盡管程序仍然會編譯,但在運行它的時候,就會得到一個IllegalMonitorStateException(非法監視器狀態違例),而且會出現多少有點莫名其妙的一條消息:“current thread not owner”(當前線程不是所有人”。注意sleep(),suspend()以及resume()都能在不同步的方法內調用,因為它們不需要對鎖定進行操作。
只能為自己的鎖定調用wait()和notify()。同樣地,仍然可以編譯那些試圖使用錯誤鎖定的代碼,但和往常一樣會產生同樣的IllegalMonitorStateException違例。我們沒辦法用其他人的對象鎖來愚弄系統,但可要求另一個對象執行相應的操作,對它自己的鎖進行操作。所以一種做法是創建一個同步方法,令其為自己的對象調用notify()。但在Notifier中,我們會看到一個同步方法內部的notify():
synchronized(wn2) { wn2.notify(); }
其中,wn2是類型為WaitNotify2的對象。盡管並不屬於WaitNotify2的一部分,這個方法仍然獲得了wn2對象的鎖定。在這個時候,它為wn2調用notify()是合法的,不會得到IllegalMonitorStateException違例。
///:Continuing /////////// Blocking via wait() /////////// class WaitNotify1 extends Blockable { public WaitNotify1(Container c) { super(c); } public synchronized void run() { while(true) { i++; update(); try { wait(1000); } catch (InterruptedException e){} } } } class WaitNotify2 extends Blockable { public WaitNotify2(Container c) { super(c); new Notifier(this); } public synchronized void run() { while(true) { i++; update(); try { wait(); } catch (InterruptedException e){} } } } class Notifier extends Thread { private WaitNotify2 wn2; public Notifier(WaitNotify2 wn2) { this.wn2 = wn2; start(); } public void run() { while(true) { try { sleep(2000); } catch (InterruptedException e){} synchronized(wn2) { wn2.notify(); } } } } ///:Continued
若必須等候其他某些條件(從線程外部加以控制)發生變化,同時又不想在線程內一直傻乎乎地等下去,一般就需要用到wait()。wait()允許我們將線程置入“睡眠”狀態,同時又“積極”地等待條件發生改變。而且只有在一個notify()或notifyAll()發生變化的時候,線程才會被喚醒,並檢查條件是否有變。因此,我們認為它提供了在線程間進行同步的一種手段。
4. IO堵塞
若一個數據流必須等候一些IO活動,便會自動進入“堵塞”狀態。在本例下面列出的部分中,有兩個類協同通用的Reader以及Writer對象工作(使用Java 1.1的流)。但在測試模型中,會設置一個管道化的數據流,使兩個線程相互間能安全地傳遞數據(這正是使用管道流的目的)。
Sender將數據置入Writer,並“睡眠”隨機長短的時間。然而,Receiver本身並沒有包括sleep(),suspend()或者wait()方法。但在執行read()的時候,如果沒有數據存在,它會自動進入“堵塞”狀態。如下所示:
///:Continuing class Sender extends Blockable { // send private Writer out; public Sender(Container c, Writer out) { super(c); this.out = out; } public void run() { while(true) { for(char c = 'A'; c <= 'z'; c++) { try { i++; out.write(c); state.setText("Sender sent: " + (char)c); sleep((int)(3000 * Math.random())); } catch (InterruptedException e){} catch (IOException e) {} } } } } class Receiver extends Blockable { private Reader in; public Receiver(Container c, Reader in) { super(c); this.in = in; } public void run() { try { while(true) { i++; // Show peeker it's alive // Blocks until characters are there: state.setText("Receiver read: " + (char)in.read()); } } catch(IOException e) { e.printStackTrace();} } } ///:Continued
這兩個類也將信息送入自己的state字段,並修改i值,使Peeker知道線程仍在運行。
5. 測試
令人驚訝的是,主要的程序片(Applet)類非常簡單,這是大多數工作都已置入Blockable框架的緣故。大概地說,我們創建了一個由Blockable對象構成的數組。而且由於每個對象都是一個線程,所以在按下“start”按鈕後,它們會采取自己的行動。還有另一個按鈕和actionPerformed()從句,用於中止所有Peeker對象。由於Java 1.2“反對”使用Thread的stop()方法,所以可考慮采用這種折衷形式的中止方式。
為了在Sender和Receiver之間建立一個連接,我們創建了一個PipedWriter和一個PipedReader。注意PipedReader in必須通過一個構建器參數同PipedWriterout連接起來。在那以後,我們在out內放進去的所有東西都可從in中提取出來——似乎那些東西是通過一個“管道”傳輸過去的。隨後將in和out對象分別傳遞給Receiver和Sender構建器;後者將它們當作任意類型的Reader和Writer看待(也就是說,它們被“上溯”造型了)。
Blockable句柄b的數組在定義之初並未得到初始化,因為管道化的數據流是不可在定義前設置好的(對try塊的需要將成為障礙):
///:Continuing /////////// Testing Everything /////////// public class Blocking extends Applet { private Button start = new Button("Start"), stopPeekers = new Button("Stop Peekers"); private boolean started = false; private Blockable[] b; private PipedWriter out; private PipedReader in; public void init() { out = new PipedWriter(); try { in = new PipedReader(out); } catch(IOException e) {} b = new Blockable[] { new Sleeper1(this), new Sleeper2(this), new SuspendResume1(this), new SuspendResume2(this), new WaitNotify1(this), new WaitNotify2(this), new Sender(this, out), new Receiver(this, in) }; start.addActionListener(new StartL()); add(start); stopPeekers.addActionListener( new StopPeekersL()); add(stopPeekers); } class StartL implements ActionListener { public void actionPerformed(ActionEvent e) { if(!started) { started = true; for(int i = 0; i < b.length; i++) b[i].start(); } } } class StopPeekersL implements ActionListener { public void actionPerformed(ActionEvent e) { // Demonstration of the preferred // alternative to Thread.stop(): for(int i = 0; i < b.length; i++) b[i].stopPeeker(); } } public static void main(String[] args) { Blocking applet = new Blocking(); Frame aFrame = new Frame("Blocking"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(350,550); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~
在init()中,注意循環會遍歷整個數組,並為頁添加state和peeker.status文本字段。
首次創建好Blockable線程以後,每個這樣的線程都會自動創建並啟動自己的Peeker。所以我們會看到各個Peeker都在Blockable線程啟動之前運行起來。這一點非常重要,因為在Blockable線程啟動的時候,部分Peeker會被堵塞,並停止運行。弄懂這一點,將有助於我們加深對“堵塞”這一概念的認識。