- 写在前面的话
- 引言
- 第 1 章 对象入门
- 第 2 章 一切都是对象
- 第 3 章 控制程序流程
- 第 4 章 初始化和清除
- 第 5 章 隐藏实施过程
- 第 6 章 类再生
- 第 7 章 多形性
- 第 8 章 对象的容纳
- 第 9 章 违例差错控制
- 第 10 章 Java IO 系统
- 第 11 章 运行期类型鉴定
- 第 12 章 传递和返回对象
- 第 十三 章 创建窗口和程序片
- 第 14 章 多线程
- 第 15 章 网络编程
- 第 16 章 设计范式
- 第 17 章 项目
- 附录 A 使用非 JAVA 代码
- 附录 B 对比 C++和 Java
- 附录 C Java 编程规则
- 附录 D 性能
- 附录 E 关于垃圾收集的一些话
- 附录 F 推荐读物
14.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. 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 会被堵塞,并停止运行。弄懂这一点,将有助于我们加深对“堵塞”这一概念的认识。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论