按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
…………………………………………………………Page 488……………………………………………………………
第 14 章 多线程
利用对象,可将一个程序分割成相互独立的区域。我们通常也需要将一个程序转换成多个独立运行的子任
务。
象这样的每个子任务都叫作一个“线程”(Thread)。编写程序时,可将每个线程都想象成独立运行,而且
都有自己的专用CPU。一些基础机制实际会为我们自动分割CPU 的时间。我们通常不必关心这些细节问题,
所以多线程的代码编写是相当简便的。
这时理解一些定义对以后的学习狠有帮助。“进程”是指一种“自包容”的运行程序,有自己的地址空间。
“多任务”操作系统能同时运行多个进程(程序)——但实际是由于CPU 分时机制的作用,使每个进程都能
循环获得自己的CPU 时间片。但由于轮换速度非常快,使得所有程序好象是在“同时”运行一样。“线程”
是进程内部单一的一个顺序控制流。因此,一个进程可能容纳了多个同时执行的线程。
多线程的应用范围很广。但在一般情况下,程序的一些部分同特定的事件或资源联系在一起,同时又不想为
它而暂停程序其他部分的执行。这样一来,就可考虑创建一个线程,令其与那个事件或资源关联到一起,并
让它独立于主程序运行。一个很好的例子便是“Quit ”或“退出”按钮——我们并不希望在程序的每一部分
代码中都轮询这个按钮,同时又希望该按钮能及时地作出响应(使程序看起来似乎经常都在轮询它)。事实
上,多线程最主要的一个用途就是构建一个“反应灵敏”的用户界面。
14。1 反应灵敏的用户界面
作为我们的起点,请思考一个需要执行某些CPU 密集型计算的程序。由于 CPU “全心全意”为那些计算服
务,所以对用户的输入十分迟钝,几乎没有什么反应。在这里,我们用一个合成的applet/application (程
序片/应用程序)来简单显示出一个计数器的结果:
//: Counter1。java
// A non…responsive user interface
package c14;
import java。awt。*;
import java。awt。event。*;
import java。applet。*;
public class Counter1 extends Applet {
private int count = 0;
private Button
onOff = new Button(〃Toggle〃);
start = new Button(〃Start〃);
private TextField t = new TextField(10);
private boolean runFlag = true;
public void init() {
add(t);
start。addActionListener(new StartL());
add(start);
onOff。addActionListener(new OnOffL());
add(onOff);
}
public void go() {
while (true) {
try {
Thread。currentThread()。sleep(100);
} catch (InterruptedException e){}
if(runFlag)
t。setText(Integer。toString(count++));
487
…………………………………………………………Page 489……………………………………………………………
}
}
class StartL implements ActionListener {
public void actionPerformed(ActionEvent e) {
go();
}
}
class OnOffL implements ActionListener {
public void actionPerformed(ActionEvent e) {
runFlag = !runFlag;
}
}
public static void main(String'' args) {
Counter1 applet = new Counter1();
Frame aFrame = new Frame(〃Counter1〃);
aFrame。addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System。exit(0);
}
});
aFrame。add(applet; BorderLayout。CENTER);
aFrame。setSize(300;200);
applet。init();
applet。start();
aFrame。setVisible(true);
}
} ///:~
在这个程序中,AWT 和程序片代码都应是大家熟悉的,第 13章对此已有很详细的交待。go()方法正是程序全
心全意服务的对待:将当前的 count (计数)值置入TextField (文本字段)t,然后使count 增值。
go() 内的部分无限循环是调用sleep()。sleep()必须同一个 Thread (线程)对象关联到一起,而且似乎每个
应用程序都有部分线程同它关联(事实上,Java 本身就是建立在线程基础上的,肯定有一些线程会伴随我们
写的应用一起运行)。所以无论我们是否明确使用了线程,都可利用Thread。currentThread()产生由程序使
用的当前线程,然后为那个线程调用sleep()。注意,Thread。currentThread()是 Thread 类的一个静态方
法。
注意 sleep()可能“掷”出一个 InterruptException (中断违例)——尽管产生这样的违例被认为是中止线
程的一种“恶意”手段,而且应该尽可能地杜绝这一做法。再次提醒大家,违例是为异常情况而产生的,而
不是为了正常的控制流。在这里包含了对一个“睡眠”线程的中断,以支持未来的一种语言特性。
一旦按下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)