1 4 . 3 堵塞
一个线程可以有四种状态:
(1) 新(New):线程对象已经创建,但尚未启动,所以不可运行。
(2) 可运行(Runnable ):意味着一旦时间分片机制有空闲的CPU 周期提供给一个线程,那个线程便可立即
开始运行。因此,线程可能在、也可能不在运行当中,但一旦条件许可,没有什么能阻止它的运行——它既
没有“死”掉,也未被“堵塞”。
(3) 死(Dead):从自己的run()方法中返回后,一个线程便已“死”掉。亦可调用stop()令其死掉,但会
产生一个违例——属于Error 的一个子类(也就是说,我们通常不捕获它)。记住一个违例的“掷”出应当
是一个特殊事件,而不是正常程序运行的一部分。所以不建议你使用stop()(在Java 1.2 则是坚决反
对)。另外还有一个destroy()方法(它永远不会实现),应该尽可能地避免调用它,因为它非常武断,根
本不会解除对象的锁定。
(4) 堵塞(Blocked):线程可以运行,但有某种东西阻碍了它。若线程处于堵塞状态,调度机制可以简单地
跳过它,不给它分配任何CPU 时间。除非线程再次进入“可运行”状态,否则不会采取任何操作。
1 4 . 3 . 1 为何会堵塞
堵塞状态是前述四种状态中最有趣的,值得我们作进一步的探讨。线程被堵塞可能是由下述五方面的原因造
成的:
(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.
511
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 衍生出来的。
512
在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(恢复器)的独立类。演示暂停/恢复过程的每个类都有一个相关的恢复器。如下所示:
513
///: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()方法。同样地,当我们启动这个线程以后,就会发现与它关联的
514
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){}
}
515
}
}
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
516
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 看待(也就是说,它们
被“上溯”造型了)。
517
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) {
518
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 会被堵塞,并停止运行。弄懂这一点,将有助于我们加深对“堵塞”这一概念的认识。
一个线程可以有四种状态:
(1) 新(New):线程对象已经创建,但尚未启动,所以不可运行。
(2) 可运行(Runnable ):意味着一旦时间分片机制有空闲的CPU 周期提供给一个线程,那个线程便可立即
开始运行。因此,线程可能在、也可能不在运行当中,但一旦条件许可,没有什么能阻止它的运行——它既
没有“死”掉,也未被“堵塞”。
(3) 死(Dead):从自己的run()方法中返回后,一个线程便已“死”掉。亦可调用stop()令其死掉,但会
产生一个违例——属于Error 的一个子类(也就是说,我们通常不捕获它)。记住一个违例的“掷”出应当
是一个特殊事件,而不是正常程序运行的一部分。所以不建议你使用stop()(在Java 1.2 则是坚决反
对)。另外还有一个destroy()方法(它永远不会实现),应该尽可能地避免调用它,因为它非常武断,根
本不会解除对象的锁定。
(4) 堵塞(Blocked):线程可以运行,但有某种东西阻碍了它。若线程处于堵塞状态,调度机制可以简单地
跳过它,不给它分配任何CPU 时间。除非线程再次进入“可运行”状态,否则不会采取任何操作。
1 4 . 3 . 1 为何会堵塞
堵塞状态是前述四种状态中最有趣的,值得我们作进一步的探讨。线程被堵塞可能是由下述五方面的原因造
成的:
(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.
511
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 衍生出来的。
512
在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(恢复器)的独立类。演示暂停/恢复过程的每个类都有一个相关的恢复器。如下所示:
513
///: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()方法。同样地,当我们启动这个线程以后,就会发现与它关联的
514
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){}
}
515
}
}
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
516
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 看待(也就是说,它们
被“上溯”造型了)。
517
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) {
518
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 会被堵塞,并停止运行。弄懂这一点,将有助于我们加深对“堵塞”这一概念的认识。