您的位置:首页 > 编程语言 > Java开发

Java编程思想——并发(1)

2010-07-28 08:28 288 查看
对象技术使你得以把程序划分成若干独立的部分。通常,你还需要把程序转换成彼此分离的,能独立运行的子任务。

每一个这些独立的子任务都被称为一个“线程”(thread)。你要这样去编写程序:每个线程都好象是在独自运行并且占有自己的处理器。处理器时间确实是通过某些底层机制进行分配的,不过一般来说,你不必考虑这些,这使得编写多线程程序的任务变得容易得多了。

所谓“进程”(process),是一个独立运行着的程序,它有自己的地址空间。“多任务”(multitasking)操作系统通过周期性地将处理器切换到不同的任务,使其能够同时运行不止一个进程(程序),而每个进程都象是连续运行、一气呵成的。线程是进程内部的单一控制序列流。因此一个进程内可以具有多个并发执行的线程。

多线程有多种用途,不过通常用法是,你的程序中的某个部分与一个特定的事件或资源联系了在一起,而你又不想让这种联系阻碍程序其余部分的运行。这时,可以创建一个与这个事件或资源相关联的线程,并且让此线程独立于主程序运行。

学习并发编程就像进入了一个全新的领域,有点类似于学习一门新的编程语言,或者至少要学习一整套新的语言概念。随着对线程的支持在大多数微机操作系统中的出现,在编程语言和程序库中也出现了对线程的扩展。总的来说,线程编程:

1.不仅看起来神秘,而且需要你改变编程时的思维方式。

2.各种语言中对线程的支持都很相似,所以只要理解了线程概念,那么在别的语言中要用到线程的话就有了共同语言。

尽管对线程的支持使Java看起来更复杂,不过这并不全是Java的错,线程本身就很讲究技巧。

要理解并发编程,其难度与理解多态机制差不多。如果你花了工夫,就能明白其基本机制,但要想真正地掌握它的实质,就需要深入的学习和理解。本章的目标就是要让你对并发的基本知识打下坚实的基础,使你能够理解其概念并编写出合理的多线程程序。注意,你可能会很容易就变得过分自信,所以在你编写任何复杂程序之前,应该学习一下专门讨论这个主题的书籍。


动机

使用并发最强制性的原因之一就是要产生能够作出响应的用户界面。考虑一个程序,它要执行某项CPU深度占用的计算,这样就会导致用户的输入被忽略,也就无法作出响应。问题的实质是:程序需要一边连续进行计算,同时还要把控制权交给用户界面,这样程序才能响应用户的操作。如果你有一个“退出”按钮,你一定不希望在程序的每段代码里都检测按钮状态,但你还是希望对这个按钮能够作出响应,就好像你定期对其进行检测一样。

传统的方法不可能一边连续执行其操作,同时又把控制权交给程序的其余部分。事实上,这听起来就像是不可能完成的任务,就好象让一个处理器同时出现在两个地方,但这恰恰是并发编程所能够提供的错觉效果。

并发还可以用来优化程序的吞吐量。比如,在你等待数据到达输入/输出端口的时候,你可以进行其他重要的工作。要是不用线程的话,唯一可行的办法就是不断查询输入/输出端口,这种方法不仅笨拙,而且很困难。

如果你有一台有多处理器的机器,多个线程就可以分布在多个处理器上,这可以极大地提高吞吐量。这种情况通常出现在有多个强劲处理器的web服务器上,在这种环境下,程序对于每个用户请求都将分配一个线程,这样就可以把大量的请求分配给多个处理器来处理。

需要牢记的是,具有多个线程的程序,必须也能够在只有单处理器的机器上运行。因此,不使用任何线程而写出具有同样功能的程序也是可能的。然而,使用多线程的重要好处是可以使程序的组织更有条理,因而能大大简化程序设计。对于某些类型的问题,比如模拟视频游戏,如果没有对并发的支持将会非常难以解决。

线程模型为编程带来了便利,它简化了在单一程序中交织在一起同时运行的多个操作。在使用线程时,处理器将轮流给每个线程分配其占用时间。每个线程都觉得自己在一直占用处理器,但事实上处理器时间是划分成片段分配给了所有的线程。例外情况是程序运行在具有多个处理器的机器上,但线程的一大好处是可以使你从这个层次抽身出来,即代码不必知道它是运行在具有一个还是多个处理器的机器上。所以,线程是一种建立透明的、可扩展的程序的方法,如果程序运行得太慢,为机器增添一个处理器就能很容易地加快程序的运行速度。多任务和多线程往往是使用多处理机系统的最合理方式。

在单处理器机器上,线程会降低一些运行效率,但是,从程序设计、资源平衡、用户使用方便等方面来看,还是非常值得的。一般来说,线程使你能得到更加松散耦合的设计;否则的话,你将不得不在部分代码中直接关注那些通常由线程处理的工作。


基本线程

写一个线程最简单的做法是从java.lang.Thread继承,这个类已经具有了创建和运行线程所必要的架构。Thread最重要的方法是run( ),你得重载这个方法,以实现你要的功能。这样,run()里的代码就能够与程序里的其它线程“同时”执行。

下面的例子创建了五个线程,每个线程由一个唯一的数字来标识,这个数字由一个静态成员变量产生。Thread类的run( )方法被重载,它的动作是在每次循环里计数值减一,当计数值为零的时候返回(在run( )方法返回的地点,将由线程机制终止此线程)。

//: c13:SimpleThread.java

// Very simple Threading example.

import com.bruceeckel.simpletest.*;

public class SimpleThread extends Thread {

private static Test monitor = new Test();

private int countDown = 5;

private static int threadCount = 0;

public SimpleThread() {

super("" + ++threadCount); // Store the thread name

start();

}

public String toString() {

return "#" + getName() + ": " + countDown;

}

public void run() {

while(true) {

System.out.println(this);

if(--countDown == 0) return;

}

}

public static void main(String[] args) {

for(int i = 0; i < 5; i++)

new SimpleThread();

monitor.expect(new String[] {

"#1: 5",

"#2: 5",

"#3: 5",

"#5: 5",

"#1: 4",

"#4: 5",

"#2: 4",

"#3: 4",

"#5: 4",

"#1: 3",

"#4: 4",

"#2: 3",

"#3: 3",

"#5: 3",

"#1: 2",

"#4: 3",

"#2: 2",

"#3: 2",

"#5: 2",

"#1: 1",

"#4: 2",

"#2: 1",

"#3: 1",

"#5: 1",

"#4: 1"

}, Test.IGNORE_ORDER + Test.WAIT);

}

} ///:~

通过调用Thread类相应的构造器,可以给线程对象指定一个名字。这个名字可以在toString( )里用getName( )得到。

Thread对象的run( )方法一般总会有某种形式的循环,使得线程一直运行下去直到不再需要,所以你要设定跳出循环的条件(或者,就像前面的程序那样,直接从run( )返回)。通常,run( )被写成无限循环的形式,这就意味着,除非有某个条件使得run( )终止,否则它将永远运行下去(在本章后面你将看到如何安全地通知线程终止)。

你可以看到在main( )里创建并运行了一些线程。Thread类的start( )方法将为线程执行特殊的初始化动作,然后调用run( )方法。所以整个步骤是:首先调用构造器来构造对象,在构造器中调用了start( )方法来配置线程,然后由线程执行机制调用run( )。如果你不调用start( )(在后面的例子你将看到,你不必在构造器里调用start( )),线程永远不会启动。

因为线程调度机制的行为不是确定性的,所以每次运行该程序都会产生不同的输出结果。实际上,你要是在不同的JDK版本下运行这个简单的程序,就会发现程序输出的差异非常大。比如,以前版本的JDK经常都是不切片时间的,所以线程1可能首先循环执行完毕,然后是线程2完成其所有循环,如此下去。这样的做法除了启动这些线程开销更加昂贵以外,在实质上,与调用一个子程序然后马上完成该子程序所有循环的做法类似。用JDK 1.4你能得到与SimpleThread.java类似的输出,这表明了调度器执行了更合适的时间切片行为,每个线程看起来都得到了有秩序的服务。总的说来,JDK这种行为上的变化并没有被Sun所提到,所以你不能对线程的行为作任何假设。应付这类问题最好的办法就是在编写线程代码时尽可能保守些。

当在main( )中创建若干个Thread对象的时候,并没有获得它们中任何一个的引用。对于普通的对象,这会使它成为垃圾回收器要回收的目标,但对于Thread对象就不会了。每个Thread对象需要“注册”自己,所以实际上在某个地方存在着对它的引用,垃圾收集器只有在线程离开了run( )并且死亡之后才能把它清理掉。

让步

如果你知道run( )方法中已经完成了所需的工作,你可以给线程调度机制一个暗示:你的工作已经做得差不多了,可以让别的线程使用处理器了。这个暗示将通过调用yield( )方法的形式来作出。(不过这只是一个暗示,没有任何机制保证它将会被采纳。)

我们可以修改前面的例子,在每次循环之后调用yield( )。

//: c13:YieldingThread.java

// Suggesting when to switch threads with yield().

import com.bruceeckel.simpletest.*;

public class YieldingThread extends Thread {

private static Test monitor = new Test();

private int countDown = 5;

private static int threadCount = 0;

public YieldingThread() {

super("" + ++threadCount);

start();

}

public String toString() {

return "#" + getName() + ": " + countDown;

}

public void run() {

while(true) {

System.out.println(this);

if(--countDown == 0) return;

yield();

}

}

public static void main(String[] args) {

for(int i = 0; i < 5; i++)

new YieldingThread();

monitor.expect(new String[] {

"#1: 5",

"#2: 5",

"#4: 5",

"#5: 5",

"#3: 5",

"#1: 4",

"#2: 4",

"#4: 4",

"#5: 4",

"#3: 4",

"#1: 3",

"#2: 3",

"#4: 3",

"#5: 3",

"#3: 3",

"#1: 2",

"#2: 2",

"#4: 2",

"#5: 2",

"#3: 2",

"#1: 1",

"#2: 1",

"#4: 1",

"#5: 1",

"#3: 1"

}, Test.IGNORE_ORDER + Test.WAIT);

}

} ///:~

使用yield( )以后,程序的输出显得比较均衡。但要注意的是,如果输出的字符串再长一点的话,你就会得到与SimpleThread.java大致相同的输出。你可以试一试,逐步改变toString( )方法,每次输出更长的字符串,以观察效果。因为调度机制是抢占式的,它能决定在需要的时候中断一个线程并切换到别的线程,所以如果输入/输出(在main( )所在的线程内执行)占用了太多的时间,它将在run( )有机会调用yield( )之前被中断。一般来说,yield( )使用的机会并不多,你要是想对程序做认真的调整的话,就不能依赖于它。

休眠

另一种能控制线程行为的方法是调用sleep( ),这将使线程停止执行一段时间,该时间由你给定的毫秒数决定。在前面的例子里,要是把对yield( )的调用换成sleep( ),将得到如下结果:

//: c13:SleepingThread.java

// Calling sleep() to wait for awhile.

import com.bruceeckel.simpletest.*;

public class SleepingThread extends Thread {

private static Test monitor = new Test();

private int countDown = 5;

private static int threadCount = 0;

public SleepingThread() {

super("" + ++threadCount);

start();

}

public String toString() {

return "#" + getName() + ": " + countDown;

}

public void run() {

while(true) {

System.out.println(this);

if(--countDown == 0) return;

try {

sleep(100);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

public static void

main(String[] args) throws InterruptedException {

for(int i = 0; i < 5; i++)

new SleepingThread().join();

monitor.expect(new String[] {

"#1: 5",

"#1: 4",

"#1: 3",

"#1: 2",

"#1: 1",

"#2: 5",

"#2: 4",

"#2: 3",

"#2: 2",

"#2: 1",

"#3: 5",

"#3: 4",

"#3: 3",

"#3: 2",

"#3: 1",

"#4: 5",

"#4: 4",

"#4: 3",

"#4: 2",

"#4: 1",

"#5: 5",

"#5: 4",

"#5: 3",

"#5: 2",

"#5: 1"

});

}

} ///:~

在你调用sleep( )方法的时候,必须把它放在try块中,这是因为sleep( )方法在休眠时间到期之前有可能被中断。如果某人持有对此线程的引用,并且在此线程上调用了interrupt( )方法,就会发生这种情况。(如果对线程调用了wait( )或join( )方法,interrupt( )也会对线程有影响,所以对这些方法的调用也必须放在类似的try块中,我们将在后面学习这些方法)。通常,如果你想使用interrupt( )来中断一个挂起的线程,那么挂起的时候最好使用wait( )而不是sleep( ),这样就不可能在catch子句里的结束了。这里,我们把异常作为一个RuntimeException重新抛出,这遵循了“除非知道如何处理,否则不要捕获异常”的原则。

你将会发现程序的输出是确定的,每个线程都在下一个线程开始之前递减计数。这是因为在每个线程之上都使用了join( )(很快你就会学到),所以main( )在继续执行之前会等待线程结束。要是你不使用join( ),你可能会发现线程可以以任意顺序执行,这说明sleep( )也不是控制线程执行顺序的方法,它仅仅使线程停止执行一段时间。你得到的唯

一保证是线程将休眠至少100毫秒,这个时间段也可能更长,因为在休眠时间到期之后,线程调度机制也需要时间来恢复线程。

如果你必须要控制线程的执行顺序,你最好是根本不用线程,而是自己编写以特定顺序彼此控制的协作子程序。


优先权

线程的“优先权”(priority)能告诉调度程序其重要性如何。尽管处理器处理现有线程集的顺序是不确定的,但是如果有许多线程被阻塞并在等待运行,那么调度程序将倾向于让优先权最高的线程先执行。然而,这并不是意味着优先权较低的线程将得不到执行(也就是说,优先权不会导致死锁)。优先级较低的线程仅仅是执行的频率较低。

下面的程序修改了SimpleThread.java,用以演示优先权。线程的优先权是通过使用setPriority( )方法进行调整的。

//: c13:SimplePriorities.java

// Shows the use of thread priorities.

import com.bruceeckel.simpletest.*;

public class SimplePriorities extends Thread {

private static Test monitor = new Test();

private int countDown = 5;

private volatile double d = 0; // No optimization

public SimplePriorities(int priority) {

setPriority(priority);

start();

}

public String toString() {

return super.toString() + ": " + countDown;

}

public void run() {

while(true) {

// An expensive, interruptable operation:

for(int i = 1; i < 100000; i++)

d = d + (Math.PI + Math.E) / (double)i;

System.out.println(this);

if(--countDown == 0) return;

}

}

public static void main(String[] args) {

new SimplePriorities(Thread.MAX_PRIORITY);

for(int i = 0; i < 5; i++)

new SimplePriorities(Thread.MIN_PRIORITY);

monitor.expect(new String[] {

"Thread[Thread-1,10,main]: 5",

"Thread[Thread-1,10,main]: 4",

"Thread[Thread-1,10,main]: 3",

"Thread[Thread-1,10,main]: 2",

"Thread[Thread-1,10,main]: 1",

"Thread[Thread-2,1,main]: 5",

"Thread[Thread-2,1,main]: 4",

"Thread[Thread-2,1,main]: 3",

"Thread[Thread-2,1,main]: 2",

"Thread[Thread-2,1,main]: 1",

"Thread[Thread-3,1,main]: 5",

"Thread[Thread-4,1,main]: 5",

"Thread[Thread-5,1,main]: 5",

"Thread[Thread-6,1,main]: 5",

"Thread[Thread-3,1,main]: 4",

"Thread[Thread-4,1,main]: 4",

"Thread[Thread-5,1,main]: 4",

"Thread[Thread-6,1,main]: 4",

"Thread[Thread-3,1,main]: 3",

"Thread[Thread-4,1,main]: 3",

"Thread[Thread-5,1,main]: 3",

"Thread[Thread-6,1,main]: 3",

"Thread[Thread-3,1,main]: 2",

"Thread[Thread-4,1,main]: 2",

"Thread[Thread-5,1,main]: 2",

"Thread[Thread-6,1,main]: 2",

"Thread[Thread-4,1,main]: 1",

"Thread[Thread-3,1,main]: 1",

"Thread[Thread-6,1,main]: 1",

"Thread[Thread-5,1,main]: 1"

}, Test.IGNORE_ORDER + Test.WAIT);

}

} ///:~

在这个版本中,toString( )方法被重载,并在里面使用了Thread.toString( )方法来打印线程的名称(你可以通过构造器来自己设置这个名称;这里是自动生成的名称,如Thread-1,Thread-2等),线程的优先权,以及线程所属的“线程组”。因为线程是自标识的,所以本例中没有使用threadNumber。重载的toString( )方法还打印了线程的倒计数值。

你可以看到thread 1的优先权最高,其余线程的优先权被设为最低。

在run( )里,加入了100,000次开销相当大的浮点运算,包括double类型的加法与除法。变量d用volatile修饰,以确保不进行优化。如果没有加入这些运算的话,你就看不到设置优先级的效果(试一试:把包含double运算的for循环注释掉)。有了这些运算,你就能观察到thread 1被线程调度机制优先选择(至少在我的Windows 2000机器上是这样)。

尽管向控制台打印也是开销大的操作,但在那种情况下看不出优先权的效果,因为向控制台打印不能被中断(否则的话,在多线程情况下控制台显示就乱套了),而数学运算是可以中断的。运算时间要足够长,这样线程调度机制才来得及进行改变,并注意到thread 1的优先权,使其被优先选择。

对于已存在的线程,你可以用getPriority( )方法得到其优先权,也可以在任何时候使用setPriority( )方法更改其优先权(这并不局限于像SimplePriorities.java里那样在构造器中修改)。

尽管JDK有10个优先级别,但它与多数操作系统都不能映射得很好。比如,Windows 2000有7个优先级且不是固定的,所以这种映射关系也是不确定的(尽管Sun的Solaris有231个优先级)。唯一可移植的策略是当你调整优先级的时候,只使用MAX_PRIORITY,NORM_PRIORITY,和MIN_PRIORITY三种级别。


后台线程

所谓“后台”(daemon)线程,是指程序运行的时候,在后台提供一种通用服务的线程,并且这种服务并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束,程序也就终止了。反过来说,只要有任何非后台线程还在运行,程序就不会终止。比如,执行main( )的就是一个非后台线程。

//: c13:SimpleDaemons.java

// Daemon threads don't prevent the program from ending.

public class SimpleDaemons extends Thread {

public SimpleDaemons() {

setDaemon(true); // Must be called before start()

start();

}

public void run() {

while(true) {

try {

sleep(100);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

System.out.println(this);

}

}

public static void main(String[] args) {

for(int i = 0; i < 10; i++)

new SimpleDaemons();

}

} ///:~

你必须在线程启动之前调用setDaemon( )方法,才能把它设置为后台线程。在run( )里面,线程被设定为休眠一段时间。一旦所有的线程都启动了,程序马上会在所有的线程能打印信息之前立刻终止,这是因为没有非后台线程(除了main( ))使得程序保持运行。因此,程序未打印任何信息就终止了。

你可以通过调用isDaemon( )方法来确定线程是否是一个后台线程。如果是一个后台线程,那么它创建的任何线程将被自动设置成后台线程,如下例所示:

//: c13:Daemons.java

// Daemon threads spawn other daemon threads.

import java.io.*;

import com.bruceeckel.simpletest.*;

class Daemon extends Thread {

private Thread[] t = new Thread[10];

public Daemon() {

setDaemon(true);

start();

}

public void run() {

for(int i = 0; i < t.length; i++)

t[i] = new DaemonSpawn(i);

for(int i = 0; i < t.length; i++)

System.out.println("t[" + i + "].isDaemon() = "

+ t[i].isDaemon());

while(true)

yield();

}

}

class DaemonSpawn extends Thread {

public DaemonSpawn(int i) {

start();

System.out.println("DaemonSpawn " + i + " started");

}

public void run() {

while(true)

yield();

}

}

public class Daemons {

private static Test monitor = new Test();

public static void main(String[] args) throws Exception {

Thread d = new Daemon();

System.out.println("d.isDaemon() = " + d.isDaemon());

// Allow the daemon threads to

// finish their startup processes:

Thread.sleep(1000);

monitor.expect(new String[] {

"d.isDaemon() = true",

"DaemonSpawn 0 started",

"DaemonSpawn 1 started",

"DaemonSpawn 2 started",

"DaemonSpawn 3 started",

"DaemonSpawn 4 started",

"DaemonSpawn 5 started",

"DaemonSpawn 6 started",

"DaemonSpawn 7 started",

"DaemonSpawn 8 started",

"DaemonSpawn 9 started",

"t[0].isDaemon() = true",

"t[1].isDaemon() = true",

"t[2].isDaemon() = true",

"t[3].isDaemon() = true",

"t[4].isDaemon() = true",

"t[5].isDaemon() = true",

"t[6].isDaemon() = true",

"t[7].isDaemon() = true",

"t[8].isDaemon() = true",

"t[9].isDaemon() = true"

}, Test.IGNORE_ORDER + Test.WAIT);

}

} ///:~

在Daemon线程中把后台标志设置为“真”,然后派生出许多子线程,这些线程并没有被明确设置是否为后台线程,不过它们的确是后台线程。接着,线程进入了无限循环,并在循环里调用yield( )方法来把控制权交给其它进程。

一旦main( )完成其工作,就没什么能阻止程序终止了,因为除了后台线程之外,已经没有线程在运行了。main( )线程被设定为睡眠一段时间,所以你可以观察到所有后台线程启动后的结果。不这样的话,你就只能看见一些后台线程创建时得到的结果。(试试调整sleep( )休眠的时间,以观察这个行为。)


加入到某个线程

一个线程可以在其它线程之上调用join( )方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程t上调用t.join( ),此线程将被挂起,直到目标线程t结束才恢复(即t.isAlive( )返回为假)。

你也可以在调用join( )时带上一个超时参数(单位可以是毫秒或者毫秒加纳秒),这样如果目标线程在这段时间到期时还没有结束的话,join( )方法总能返回。

对join( )方法的调用可以被中断,做法是在调用线程上调用interrupt( )方法,这时需要用到try-catch子句。

下面这个例子演示了所有这些操作:

//: c13:Joining.java

// Understanding join().

import com.bruceeckel.simpletest.*;

class Sleeper extends Thread {

private int duration;

public Sleeper(String name, int sleepTime) {

super(name);

duration = sleepTime;

start();

}

public void run() {

try {

sleep(duration);

} catch (InterruptedException e) {

System.out.println(getName() + " was interrupted. " +

"isInterrupted(): " + isInterrupted());

return;

}

System.out.println(getName() + " has awakened");

}

}

class Joiner extends Thread {

private Sleeper sleeper;

public Joiner(String name, Sleeper sleeper) {

super(name);

this.sleeper = sleeper;

start();

}

public void run() {

try {

sleeper.join();

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

System.out.println(getName() + " join completed");

}

}

public class Joining {

private static Test monitor = new Test();

public static void main(String[] args) {

Sleeper

sleepy = new Sleeper("Sleepy", 1500),

grumpy = new Sleeper("Grumpy", 1500);

Joiner

dopey = new Joiner("Dopey", sleepy),

doc = new Joiner("Doc", grumpy);

grumpy.interrupt();

monitor.expect(new String[] {

"Grumpy was interrupted. isInterrupted(): false",

"Doc join completed",

"Sleepy has awakened",

"Dopey join completed"

}, Test.AT_LEAST + Test.WAIT);

}

} ///:~

Sleeper是一个Thread类型,它要休眠一段时间,这段时间是通过构造器传进来的参数所指定的。在run( )中,sleep( )方法有可能在指定的时间期满时返回,但也可能被中断。在catch子句中,将根据isInterrupted( )的返回值报告这个中断。当另一个线程在该线程上调用interrupt( )时,将给该线程设定一个标志,表明该线程已经被中断。然而,异常被捕获时将清除这个标志,所以在异常被捕获的时候这个标志总是为假。除异常之外,这个标志还可用于其它情况,比如线程可能会检查其中断状态。

Joiner线程将通过在Sleeper对象上调用join( )方法来等待Sleeper醒来。在main( )里面,每个Sleeper都有一个Joiner,你可以在输出中发现,如果Sleeper被中断或者是正常结束,Joiner将和Sleeper一同结束。


编码的变体

到目前为止你所看到的所有简单例子中,线程对象都继承自Thread。这么做很合理,因为很显然,这些对象仅仅是作为线程而创建的,并不具有其它任何行为。然而,你的类也许已经继承了其它的类,在这种情况下,就不可能同时继承Thread(Java并不支持多重继承)。这时,你可以使用“实现Runnable接口” 的方法作为替代。要实现Runnable接口,只需实现run( )方法,Thread也是从Runnable接口实现而来的。

以下例子演示了这种方法的要点:

//: c13:RunnableThread.java

// SimpleThread using the Runnable interface.

public class RunnableThread implements Runnable {

private int countDown = 5;

public String toString() {

return "#" + Thread.currentThread().getName() +

": " + countDown;

}

public void run() {

while(true) {

System.out.println(this);

if(--countDown == 0) return;

}

}

public static void main(String[] args) {

for(int i = 1; i <= 5; i++)

new Thread(new RunnableThread(), "" + i).start();

// Output is like SimpleThread.java

}

} ///:~

Runnable类型的类只需一个run( )方法,但是如果你想要对这个Thread对象做点别的事情(比如在toString( )里调用getName( )),那么你就必须通过调用Thread.currentThread( )方法明确得到对此线程的引用。这里采用的Thread构造器接受一个Runnable对象和一个线程名称作为其参数。

当某个对象具有Runnable接口,即表明它有run( )方法,但也就仅此而已,不像从Thread继承的那些类,它本身并不带有任何和线程有关的特性。所以要从Runnable对象产生一个线程,你必须像例子中那样建立一个单独的Thread对象,并把Runnable对象传给专门的Thread构造器。然后你可以对这个线程对象调用start( ),去执行一些通常的初始化动作,然后调用run( )。

Runnable接口的方便之处在于所有事物都属于同一个类;也就是说,Runnable允许把基类和其它接口混在一起。如果你要访问某些资源,只需直接去做,而不用通过别的对象。不过,内部类也能同样方便地访问外部类的所有部分,所以,成员访问并不是使用Runnalbe接口形成混和类,而不是“一个Thread子类类型的内部类”的强制因素。

当你使用了Runnable,你通常的意思就是,要用run( )方法中所实现的这段代码创建一个进程(process),而不是创建一个对象表示该进程。这一点是有争议的,取决于你认为把线程作为一个对象来表示,或是作为完全不同的一个实体,即进程来表示,这两种方式哪一种更具实际意义1。如果你觉得应该是一个进程,你就不必拘泥于面向对象的原则,即“所有事物都是对象”。这也意味着,如果仅仅是想开启一个进程以驱动程序的某个部分,就没有理由把整个类写成是Runnable类型的。因此,使用内部类把和线程有关的代码隐藏在类的内部,似乎更合理,如下所示:

//: c13:ThreadVariations.java

// Creating threads with inner classes.

import com.bruceeckel.simpletest.*;

// Using a named inner class:

class InnerThread1 {

private int countDown = 5;

private Inner inner;

private class Inner extends Thread {

Inner(String name) {

super(name);

start();

}

public void run() {

while(true) {

System.out.println(this);

if(--countDown == 0) return;

try {

sleep(10);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

public String toString() {

return getName() + ": " + countDown;

}

}

public InnerThread1(String name) {

1 Java 1.0已经支持Runnable,而Java 1.1才引入内部类,这也部分说明了Runnable存在的原因。此外,传统的多线程模式着眼于要运行的功能,而不是表示成对象。我的习惯是尽可能从Thread继承;这样看起来更清楚和灵活。

inner = new Inner(name);

}

}

// Using an anonymous inner class:

class InnerThread2 {

private int countDown = 5;

private Thread t;

public InnerThread2(String name) {

t = new Thread(name) {

public void run() {

while(true) {

System.out.println(this);

if(--countDown == 0) return;

try {

sleep(10);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

public String toString() {

return getName() + ": " + countDown;

}

};

t.start();

}

}

// Using a named Runnable implementation:

class InnerRunnable1 {

private int countDown = 5;

private Inner inner;

private class Inner implements Runnable {

Thread t;

Inner(String name) {

t = new Thread(this, name);

t.start();

}

public void run() {

while(true) {

System.out.println(this);

if(--countDown == 0) return;

try {

Thread.sleep(10);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

public String toString() {

return t.getName() + ": " + countDown;

}

}

public InnerRunnable1(String name) {

inner = new Inner(name);

}

}

// Using an anonymous Runnable implementation:

class InnerRunnable2 {

private int countDown = 5;

private Thread t;

public InnerRunnable2(String name) {

t = new Thread(new Runnable() {

public void run() {

while(true) {

System.out.println(this);

if(--countDown == 0) return;

try {

Thread.sleep(10);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

public String toString() {

return Thread.currentThread().getName() +

": " + countDown;

}

}, name);

t.start();

}

}

// A separate method to run some code as a thread:

class ThreadMethod {

private int countDown = 5;

private Thread t;

private String name;

public ThreadMethod(String name) { this.name = name; }

public void runThread() {

if(t == null) {

t = new Thread(name) {

public void run() {

while(true) {

System.out.println(this);

if(--countDown == 0) return;

try {

sleep(10);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

public String toString() {

return getName() + ": " + countDown;

}

};

t.start();

}

}

}

public class ThreadVariations {

private static Test monitor = new Test();

public static void main(String[] args) {

new InnerThread1("InnerThread1");

new InnerThread2("InnerThread2");

new InnerRunnable1("InnerRunnable1");

new InnerRunnable2("InnerRunnable2");

new ThreadMethod("ThreadMethod").runThread();

monitor.expect(new String[] {

"InnerThread1: 5",

"InnerThread2: 5",

"InnerThread2: 4",

"InnerRunnable1: 5",

"InnerThread1: 4",

"InnerRunnable2: 5",

"ThreadMethod: 5",

"InnerRunnable1: 4",

"InnerThread2: 3",

"InnerRunnable2: 4",

"ThreadMethod: 4",

"InnerThread1: 3",

"InnerRunnable1: 3",

"ThreadMethod: 3",

"InnerThread1: 2",

"InnerThread2: 2",

"InnerRunnable2: 3",

"InnerThread2: 1",

"InnerRunnable2: 2",

"InnerRunnable1: 2",

"ThreadMethod: 2",

"InnerThread1: 1",

"InnerRunnable1: 1",

"InnerRunnable2: 1",

"ThreadMethod: 1"

}, Test.IGNORE_ORDER + Test.WAIT);

}

} ///:~

InnerThread1创建了一个命名的内部类,它继承自Thread,并且在构造器中创建了一个此内部类的实例。如果你需要在别的方法中访问此内部类(比如新的方法),这么做就显得很合理。不过,绝大多数时候创建一个线程的原因仅仅是为了使用Thread的功能,所以建立一个命名的内部类往往没有必要。InnerThread2演示了另一种选择:在构造器内部建立了一个匿名内部类,它也继承自Thread,同时它被向上转型为对Thread的引用t。如果该类的别的方法需要访问t,它们可以通过Thread的接口访问,并且不需要知道这个对象的确切类型。

例子中的第三、四个类和前两个大致相同,但是它们实现了Runnable接口而不是从Thread继承。这只不过表明了在实现Runnable接口的情况下,并没有带来更多好处,而且实际上代码要稍微复杂一些(读起来也是)。所以,除非被迫使用Runnable,否则我更倾向于使用Thread。

ThreadMethod类演示了如何在方法内部创建一个线程。当你调用该方法准备运行这个线程时,在线程启动后该方法返回。当线程只是做一些辅助工作,而不是作为类的基本功能的时候,这种方案就比在类的构造器中启动一个线程显得更合理。


建立有响应的用户界面

如前所述,使用线程的动机之一就是建立有响应的用户界面。尽管我们要到第14章才接触到图形用户界面,你在本章还是可以看到一个基于控制台用户界面的简单例子。下面的例子有两个版本:一个在全神贯注于运算,所以不能读取控制台输入;另一个把运算放在线程里单独运行,此时就可以在进行运算的同时监听控制台输入。

//: c13:ResponsiveUI.java

// User interface responsiveness.

import com.bruceeckel.simpletest.*;

class UnresponsiveUI {

private volatile double d = 1;

public UnresponsiveUI() throws Exception {

while(d > 0)

d = d + (Math.PI + Math.E) / d;

System.in.read(); // Never gets here

}

}

public class ResponsiveUI extends Thread {

private static Test monitor = new Test();

private static volatile double d = 1;

public ResponsiveUI() {

setDaemon(true);

start();

}

public void run() {

while(true) {

d = d + (Math.PI + Math.E) / d;

}

}

public static void main(String[] args) throws Exception {

//! new UnresponsiveUI(); // Must kill this process

new ResponsiveUI();

Thread.sleep(300);

System.in.read(); // 'monitor' provides input

System.out.println(d); // Shows progress

}

} ///:~

UnresponsiveUI在一个无限的while循环里执行运算,显然程序不可能到达读取控制台输入的那一行(编译器被欺骗了,相信while的条件使得程序能到达读取控制台输入的那一行)。如果你把建立UnresponsiveUI的那一行解除注释再运行程序,那么要终止它的话,就只能杀死(kill)这个进程。

要想让程序有响应,就得把计算程序放在run( )方法中,这样它就能让出处理器给别的程序。当你按下回车键的时候,你可以看到计算确实在作为后台程序运行,同时还在等待用户输入(基于测试的原因,控制台输入这一行使用com.bruceeckel.simpletest.Test对象自动提交给System.in.read( ),这将在第15章中解释)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: