堵塞狀態是前述四種狀態中最有趣的,值得我們作進一步的探討。線程被堵塞可能是由下述五方面的原因造成的:
(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會被堵塞,並停止運行。弄懂這一點,將有助於我們加深對“堵塞”這一概念的認識。