Java编程思想第4版[中文版](PDF格式)-第127部分
按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
程的一种“恶意”手段,而且应该尽可能地杜绝这一做法。再次提醒大家,违例是为异常情况而产生的,而
不是为了正常的控制流。在这里包含了对一个“睡眠”线程的中断,以支持未来的一种语言特性。
一旦按下start 按钮,就会调用go()。研究一下go(),你可能会很自然地(就象我一样)认为它该支持多线
程,因为它会进入“睡眠”状态。也就是说,尽管方法本身“睡着”了,CPU 仍然应该忙于监视其他按钮
“按下”事件。但有一个问题,那就是go()是永远不会返回的,因为它被设计成一个无限循环。这意味着
actionPerformed()根本不会返回。由于在第一个按键以后便陷入 actionPerformed()中,所以程序不能再对
其他任何事件进行控制(如果想出来,必须以某种方式“杀死”进程——最简便的方式就是在控制台窗口按
Ctrl +C 键)。
这里最基本的问题是go()需要继续执行自己的操作,而与此同时,它也需要返回,以便 actionPerformed()
能够完成,而且用户界面也能继续响应用户的操作。但对象go()这样的传统方法来说,它却不能在继续的同
时将控制权返回给程序的其他部分。这听起来似乎是一件不可能做到的事情,就象CPU 必须同时位于两个地
方一样,但线程可以解决一切。“线程模型”(以及Java 中的编程支持)是一种程序编写规范,可在单独一
个程序里实现几个操作的同时进行。根据这一机制,CPU 可为每个线程都分配自己的一部分时间。每个线程
都“感觉”自己好象拥有整个 CPU,但CPU 的计算时间实际却是在所有线程间分摊的。
线程机制多少降低了一些计算效率,但无论程序的设计,资源的均衡,还是用户操作的方便性,都从中获得
488
…………………………………………………………Page 490……………………………………………………………
了巨大的利益。综合考虑,这一机制是非常有价值的。当然,如果本来就安装了多块 CPU,那么操作系统能
够自行决定为不同的CPU 分配哪些线程,程序的总体运行速度也会变得更快(所有这些都要求操作系统以及
应用程序的支持)。多线程和多任务是充分发挥多处理机系统能力的一种最有效的方式。
14。1。1 从线程继承
为创建一个线程,最简单的方法就是从Thread 类继承。这个类包含了创建和运行线程所需的一切东西。
Thread 最重要的方法是run()。但为了使用run(),必须对其进行过载或者覆盖,使其能充分按自己的吩咐
行事。因此,run()属于那些会与程序中的其他线程“并发”或“同时”执行的代码。
下面这个例子可创建任意数量的线程,并通过为每个线程分配一个独一无二的编号(由一个静态变量产
生),从而对不同的线程进行跟踪。Thread 的run()方法在这里得到了覆盖,每通过一次循环,计数就减
1——计数为 0 时则完成循环(此时一旦返回 run(),线程就中止运行)。
//: SimpleThread。java
// Very simple Threading example
public class SimpleThread extends Thread {
private int countDown = 5;
private int threadNumber;
private static int threadCount = 0;
public SimpleThread() {
threadNumber = ++threadCount;
System。out。println(〃Making 〃 + threadNumber);
}
public void run() {
while(true) {
System。out。println(〃Thread 〃 +
threadNumber + 〃(〃 + countDown + 〃)〃);
if(……countDown == 0) return;
}
}
public static void main(String'' args) {
for(int i = 0; i 《 5; i++)
new SimpleThread()。start();
System。out。println(〃All Threads Started〃);
}
} ///:~
run()方法几乎肯定含有某种形式的循环——它们会一直持续到线程不再需要为止。因此,我们必须规定特定
的条件,以便中断并退出这个循环(或者在上述的例子中,简单地从run()返回即可)。run()通常采用一种
无限循环的形式。也就是说,通过阻止外部发出对线程的 stop()或者destroy()调用,它会永远运行下去
(直到程序完成)。
在main()中,可看到创建并运行了大量线程。Thread 包含了一个特殊的方法,叫作 start(),它的作用是对
线程进行特殊的初始化,然后调用 run()。所以整个步骤包括:调用构建器来构建对象,然后用start()配置
线程,再调用run()。如果不调用start()——如果适当的话,可在构建器那样做——线程便永远不会启动。
下面是该程序某一次运行的输出(注意每次运行都会不同):
Making 1
Making 2
Making 3
Making 4
Making 5
Thread 1(5)
489
…………………………………………………………Page 491……………………………………………………………
Thread 1(4)
Thread 1(3)
Thread 1(2)
Thread 2(5)
Thread 2(4)
Thread 2(3)
Thread 2(2)
Thread 2(1)
Thread 1(1)
All Threads Started
Thread 3(5)
Thread 4(5)
Thread 4(4)
Thread 4(3)
Thread 4(2)
Thread 4(1)
Thread 5(5)
Thread 5(4)
Thread 5(3)
Thread 5(2)
Thread 5(1)
Thread 3(4)
Thread 3(3)
Thread 3(2)
Thread 3(1)
可注意到这个例子中到处都调用了 sleep(),然而输出结果指出每个线程都获得了属于自己的那一部分CPU
执行时间。从中可以看出,尽管 sleep()依赖一个线程的存在来执行,但却与允许或禁止线程无关。它只不
过是另一个不同的方法而已。
亦可看出线程并不是按它们创建时的顺序运行的。事实上,CPU 处理一个现有线程集的顺序是不确定的——
除非我们亲自介入,并用Thread 的setPriority()方法调整它们的优先级。
main()创建 Thread 对象时,它并未捕获任何一个对象的句柄。普通对象对于垃圾收集来说是一种 “公平竞
赛”,但线程却并非如此。每个线程都会“注册”自己,所以某处实际存在着对它的一个引用。这样一来,
垃圾收集器便只好对它“瞠目以对”了。
14。1。2 针对用户界面的多线程
现在,我们也许能用一个线程解决在 Counter1。java 中出现的问题。采用的一个技巧便是在一个线程的 run()
方法中放置“子任务”——亦即位于go() 内的循环。一旦用户按下Start 按钮,线程就会启动,但马上结束
线程的创建。这样一来,尽管线程仍在运行,但程序的主要工作却能得以继续(等候并响应用户界面的事
件)。下面是具体的代码:
//: Counter2。java
// A responsive user interface with threads
import java。awt。*;
import java。awt。event。*;
import java。applet。*;
class SeparateSubTask extends Thread {
private int count = 0;
private Counter2 c2;
private boolean runFlag = true;
public SeparateSubTask(Counter2 c2) {
490
…………………………………………………………Page 492……………………………………………………………
this。c2 = c2;
start();
}
public void invertFlag() { runFlag = !runFlag;}
public void run() {
while (true) {
try {
sleep(100);
} catch (InterruptedException e){}
if(runFlag)
c2。t。setText(Integer。toString(count++));
}
}
}
public class Counter2 extends Applet {
TextField t = new TextField(10);
private SeparateSubTask sp = null;
private Button
onOff = new Button(〃Toggle〃);
start = new Button(〃Start〃);
public void init() {
add(t);
start。addActionListener(new StartL());
add(start);
onOff。addActionListener(new OnOffL());
add(onOff);
}
class StartL implements ActionListener {
public void actionPerformed(ActionEvent e) {
if(sp == null)
sp = new SeparateSubTask(Counter2。this);
}
}
class OnOffL implements ActionListener {
public void actionPerformed(ActionEvent e) {
if(sp != null)
sp。invertFlag();
}
}
public static void main(String'' args) {
Counter2 applet = new Counter2();
Frame aFrame = new Frame(〃Counter2〃);
aFrame。addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System。exit(0);
}
});
aFrame。add(applet; BorderLayout。CENTER);
aFrame。setSize(300;200);
applet。init();
491
…………………………………………………………Page 493……………………………………………………………
applet。start();
aFrame。setVisible(true);
}
} ///:~
现在,Counter2 变成了一个相当直接的程序,它的唯一任务就是设置并管理用户界面。但假若用户现在按下
Start 按钮,却不会真正调用一个方法。此时不是创建类的一个线程,而是创建 SeparateSubTask,然后继续
Counter2 事件循环。注意此时会保存 SeparateSubTask 的句柄,以便我们按下onOff 按钮的时候,能正常地
切换位于SeparateSubTask 内部的runFlag (运行标志)。随后那个线程便可启动(当它看到标志的时
候),然后将自己中止(亦可将SeparateSubTask 设为一个内部类来达到这一目的)。
SeparateSubTask 类是对 Thread 的一个简单扩展,它带有一个构建器(其中保存了 Counter2 句柄,然后通
过调用 start()来运行线程)以及一个 run()——本质上包含了Counter1。java 的go()内的代码。由于
SeparateSubTask 知道自己容纳了指向一个 Counter2 的句柄,所以能够在需要的时候介入,并访问 Counter2
的TestField (文本字段)。
按下onOff 按钮,几乎立即能得到正确的响应。当然,这个响应其实并不是“立即”发生的,它毕竟和那种
由“中断”驱动的系统不同。只有线程拥有CPU 的执行时间,并注意到标记已发生改变,计数器才会停止。
1。 用内部类改善代码
下面说说题外话,请大家注意一下 SeparateSubTask 和 Counter2 类之间发生的结合行为。SeparateSubTask
同Counter2 “亲密”地结合到了一起——它必须持有指向自己“父”Counter2 对象的一个句柄,以便自己能
回调和操纵它。但两个类并不是真的合并为单独一个类(尽管在下一节中,我们会讲到Java 确实提供了合并
它们的方法),因为它们各自做的是不同的事情,而且是在不同的时间创建的。但不管怎样,它们依然紧密
地结合到一起(更准确地说,应该叫“联合”),所以使程序代码多少显得有些笨拙。在这种情况下,一个
内部类可以显著改善代码的“可读性”和执行效率:
//: Counter2i。java
// Counter2 using an inner class for the thread
import java。awt。*;
import java。awt。event。*;
import java。applet。*;
public class Counter2i extends Applet {
private class SeparateSubTask extends Thread {
int count = 0;
boolean runFlag = true;
SeparateSubTask() { start(); }
public void run() {
while (true) {
try {
sleep(100);
} catch (InterruptedException e){}
if(runFlag)
t。setText(Integer。toString(count++));
}
}
}
private SeparateSubTask sp = null;
private TextField t = new TextField(10);
private Button
onOff = new Button(〃Toggle〃);
start = new Button(〃Start〃);
public void init() {