在本章早些时候,我曾建议大家在将一个程序片或主Frame当作Runnable的实现形式之前,一定要好好地想一想。若采用那种方式,就只能在自己的程序中使用其中的一个线程。这便限制了灵活性,一旦需要用到属于那种类型的多个线程,就会遇到不必要的麻烦。
当然,如果必须从一个类继承,而且想使类具有线程处理能力,则Runnable是一种正确的方案。本章最后一个例子对这一点进行了剖析,制作了一个RunnableCanvas类,用于为自己描绘不同的颜色(Canvas是“画布”的意思)。这个应用被设计成从命令行获得参数值,以决定颜色网格有多大,以及颜色发生变化之间的sleep()有多长。通过运用这些值,大家能体验到线程一些有趣而且可能令人费解的特性:
//: ColorBoxes.java // Using the Runnable interface import java.awt.*; import java.awt.event.*; class CBox extends Canvas implements Runnable { private Thread t; private int pause; private static final Color[] colors = { Color.black, Color.blue, Color.cyan, Color.darkGray, Color.gray, Color.green, Color.lightGray, Color.magenta, Color.orange, Color.pink, Color.red, Color.white, Color.yellow }; private Color cColor = newColor(); private static final Color newColor() { return colors[ (int)(Math.random() * colors.length) ]; } public void paint(Graphics g) { g.setColor(cColor); Dimension s = getSize(); g.fillRect(0, 0, s.width, s.height); } public CBox(int pause) { this.pause = pause; t = new Thread(this); t.start(); } public void run() { while(true) { cColor = newColor(); repaint(); try { t.sleep(pause); } catch(InterruptedException e) {} } } } public class ColorBoxes extends Frame { public ColorBoxes(int pause, int grid) { setTitle("ColorBoxes"); setLayout(new GridLayout(grid, grid)); for (int i = 0; i < grid * grid; i++) add(new CBox(pause)); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); } public static void main(String[] args) { int pause = 50; int grid = 8; if(args.length > 0) pause = Integer.parseInt(args[0]); if(args.length > 1) grid = Integer.parseInt(args[1]); Frame f = new ColorBoxes(pause, grid); f.setSize(500, 400); f.setVisible(true); } } ///:~ColorBoxes是一个典型的应用(程序),有一个构建器用于设置GUI。这个构建器采用int grid的一个参数,用它设置GridLayout(网格布局),使每一维里都有一个grid单元。随后,它添加适当数量的CBox对象,用它们填充网格,并为每一个都传递pause值。在main()中,我们可看到如何对pause和grid的默认值进行修改(如果用命令行参数传递)。
CBox是进行正式工作的地方。它是从Canvas继承的,并实现了Runnable接口,使每个Canvas也能是一个Thread。记住在实现Runnable的时候,并没有实际产生一个Thread对象,只是一个拥有run()方法的类。因此,我们必须明确地创建一个Thread对象,并将Runnable对象传递给构建器,随后调用start()(在构建器里进行)。在CBox里,这个线程的名字叫作t。
请留意数组colors,它对Color类中的所有颜色进行了列举(枚举)。它在newColor()中用于产生一种随机选择的颜色。当前的单元(格)颜色是cColor。
paint()则相当简单——只是将颜色设为cColor,然后用那种颜色填充整张画布(Canvas)。
在run()中,我们看到一个无限循环,它将cColor设为一种随机颜色,然后调用repaint()把它显示出来。随后,对线程执行sleep(),使其“休眠”由命令行指定的时间长度。
由于这种设计方案非常灵活,而且线程处理同每个Canvas元素都紧密结合在一起,所以在理论上可以生成任意多的线程(但在实际应用中,这要受到JVM能够从容对付的线程数量的限制)。
这个程序也为我们提供了一个有趣的评测基准,因为它揭示了不同JVM机制在速度上造成的戏剧性的差异。
有些时候,我们会发现ColorBoxes几乎陷于停顿状态。在我自己的机器上,这一情况在产生了10×10的网格之后发生了。为什么会这样呢?自然地,我们有理由怀疑AWT对它做了什么事情。所以这里有一个例子能够测试那个猜测,它产生了较少的线程。代码经过了重新组织,使一个Vector实现了Runnable,而且那个Vector容纳了数量众多的色块,并随机挑选一些进行更新。随后,我们创建大量这些Vector对象,数量大致取决于我们挑选的网格维数。结果便是我们得到比色块少得多的线程。所以假如有一个速度的加快,我们就能立即知道,因为前例的线程数量太多了。如下所示:
//: ColorBoxes2.java // Balancing thread use import java.awt.*; import java.awt.event.*; import java.util.*; class CBox2 extends Canvas { private static final Color[] colors = { Color.black, Color.blue, Color.cyan, Color.darkGray, Color.gray, Color.green, Color.lightGray, Color.magenta, Color.orange, Color.pink, Color.red, Color.white, Color.yellow }; private Color cColor = newColor(); private static final Color newColor() { return colors[ (int)(Math.random() * colors.length) ]; } void nextColor() { cColor = newColor(); repaint(); } public void paint(Graphics g) { g.setColor(cColor); Dimension s = getSize(); g.fillRect(0, 0, s.width, s.height); } } class CBoxVector extends Vector implements Runnable { private Thread t; private int pause; public CBoxVector(int pause) { this.pause = pause; t = new Thread(this); } public void go() { t.start(); } public void run() { while(true) { int i = (int)(Math.random() * size()); ((CBox2)elementAt(i)).nextColor(); try { t.sleep(pause); } catch(InterruptedException e) {} } } } public class ColorBoxes2 extends Frame { private CBoxVector[] v; public ColorBoxes2(int pause, int grid) { setTitle("ColorBoxes2"); setLayout(new GridLayout(grid, grid)); v = new CBoxVector[grid]; for(int i = 0; i < grid; i++) v[i] = new CBoxVector(pause); for (int i = 0; i < grid * grid; i++) { v[i % grid].addElement(new CBox2()); add((CBox2)v[i % grid].lastElement()); } for(int i = 0; i < grid; i++) v[i].go(); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); } public static void main(String[] args) { // Shorter default pause than ColorBoxes: int pause = 5; int grid = 8; if(args.length > 0) pause = Integer.parseInt(args[0]); if(args.length > 1) grid = Integer.parseInt(args[1]); Frame f = new ColorBoxes2(pause, grid); f.setSize(500, 400); f.setVisible(true); } } ///:~
在ColorBoxes2中,我们创建了CBoxVector的一个数组,并对其初始化,使其容下各个CBoxVector网格。每个网格都知道自己该“睡眠”多长的时间。随后为每个CBoxVector都添加等量的Cbox2对象,而且将每个Vector都告诉给go(),用它来启动自己的线程。
CBox2类似CBox——能用一种随机选择的颜色描绘自己。但那就是CBox2能够做的全部工作。所有涉及线程的处理都已移至CBoxVector进行。
CBoxVector也可以拥有继承的Thread,并有一个类型为Vector的成员对象。这样设计的好处就是addElement()和elementAt()方法可以获得特定的参数以及返回值类型,而不是只能获得常规Object(它们的名字也可以变得更短)。然而,这里采用的设计表面上看需要较少的代码。除此以外,它会自动保留一个Vector的其他所有行为。由于elementAt()需要大量进行“封闭”工作,用到许多括号,所以随着代码主体的扩充,最终仍有可能需要大量代码。
和以前一样,在我们实现Runnable的时候,并没有获得与Thread配套提供的所有功能,所以必须创建一个新的Thread,并将自己传递给它的构建器,以便正式“启动”——start()——一些东西。大家在CBoxVector构建器和go()里都可以体会到这一点。run()方法简单地选择Vector里的一个随机元素编号,并为那个元素调用nextColor(),令其挑选一种新的随机颜色。
运行这个程序时,大家会发现它确实变得更快,响应也更迅速(比如在中断它的时候,它能更快地停下来)。而且随着网格尺寸的壮大,它也不会经常性地陷于“停顿”状态。因此,线程的处理又多了一项新的考虑因素:必须随时检查自己有没有“太多的线程”(无论对什么程序和运行平台)。若线程太多,必须试着使用上面介绍的技术,对程序中的线程数量进行“平衡”。如果在一个多线程的程序中遇到了性能上的问题,那么现在有许多因素需要检查:
象这样的一些问题是造成多线程应用程序的编制成为一种“技术活”的原因之一。
何时使用多线程技术,以及何时避免用它,这是我们需要掌握的重要课题。骼它的主要目的是对大量任务进行有序的管理。通过多个任务的混合使用,可以更有效地利用计算机资源,或者对用户来说显得更方便。资源均衡的经典问题是在IO等候期间如何利用CPU。至于用户方面的方便性,最经典的问题就是如何在一个长时间的下载过程中监视并灵敏地反应一个“停止”(stop)按钮的按下。
多线程的主要缺点包括:
线程另一个优点是它们用“轻度”执行切换(100条指令的顺序)取代了“重度”进程场景切换(1000条指令)。由于一个进程内的所有线程共享相同的内存空间,所以“轻度”场景切换只改变程序的执行和本地变量。而在“重度”场景切换时,一个进程的改变要求必须完整地交换内存空间。
线程处理看来好象进入了一个全新的领域,似乎要求我们学习一种全新的程序设计语言——或者至少学习一系列新的语言概念。由于大多数微机操作系统都提供了对线程的支持,所以程序设计语言或者库里也出现了对线程的扩展。不管在什么情况下,涉及线程的程序设计:
(1) 刚开始会让人摸不着头脑,要求改换我们传统的编程思路;
(2) 其他语言对线程的支持看来是类似的。所以一旦掌握了线程的概念,在其他环境也不会有太大的困难。尽管对线程的支持使Java语言的复杂程度多少有些增加,但请不要责怪Java。毕竟,利用线程可以做许多有益的事情。多个线程可能共享同一个资源(比如一个对象里的内存),这是运用线程时面临的最大的一个麻烦。必须保证多个线程不会同时试图读取和修改那个资源。这要求技巧性地运用synchronized(同步)关键字。它是一个有用的工具,但必须真正掌握它,因为假若操作不当,极易出现死锁。
除此以外,运用线程时还要注意一个非常特殊的问题。由于根据Java的设计,它允许我们根据需要创建任意数量的线程——至少理论上如此(例如,假设为一项工程方面的有限元素分析创建数以百万的线程,这对Java来说并非实际)。然而,我们一般都要控制自己创建的线程数量的上限。因为在某些情况下,大量线程会将场面变得一团糟,所以工作都会几乎陷于停顿。临界点并不象对象那样可以达到几千个,而是在100以下。一般情况下,我们只创建少数几个关键线程,用它们解决某个特定的问题。这时数量的限制问题不大。但在较常规的一些设计中,这一限制确实会使我们感到束手束脚。
大家要注意线程处理中一个不是十分直观的问题。由于采用了线程“调度”机制,所以通过在run()的主循环中插入对sleep()的调用,一般都可以使自己的程序运行得更快一些。这使它对编程技巧的要求非常高,特别是在更长的延迟似乎反而能提高性能的时候。当然,之所以会出现这种情况,是由于在正在运行的线程准备进入“休眠”状态之前,较短的延迟可能造成“sleep()结束”调度机制的中断。这便强迫调度机制将其中止,并于稍后重新启动,以便它能做完自己的事情,再进入休眠状态。必须多想一想,才能意识到事情真正的麻烦程度。
本章遗漏的一件事情是一个动画例子,这是目前程序片最流行的一种应用。然而,Java JDK配套提供了解决这个问题的一整套方案(并可播放声音),大家可到java.sun.com的演示区域下载。此外,我们完全有理由相信未来版本的Java会提供更好的动画支持——尽管目前的Web涌现出了与传统方式完全不同的非Java、非程序化的许多动画方案。如果想系统学习Java动画的工作原理,可参考《Core Java——核心Java》一书,由Cornell&Horstmann编著,Prentice-Hall于1997年出版。若欲更深入地了解线程处理,请参考《Concurrent Programming in Java——Java中的并发编程》,由Doug Lea编著,Addison-Wiseley于1997年出版;或者《Java Threads——Java线程》,Oaks&Wong编著,O'Reilly于1997年出版。(1) 从Thread继承一个类,并(过载)覆盖run()方法。在run()内,打印出一条消息,然后调用sleep()。重复三遍这些操作,然后从run()返回。在构建器中放置一条启动消息,并覆盖finalize(),打印一条关闭消息。创建一个独立的线程类,使它在run()内调用System.gc()和System.runFinalization(),并打印一条消息,表明调用成功。创建这两种类型的几个线程,然后运行它们,看看会发生什么。
(2) 修改Counter2.java,使线程成为一个内部类,而且不需要明确保存指向Counter2的一个。
(3) 修改Sharing2.java,在TwoCounter的run()方法内部添加一个synchronized(同步)块,而不是同步整个run()方法。
(4) 创建两个Thread子类,第一个的run()方法用于最开始的启动,并捕获第二个Thread对象的句柄,然后调用wait()。第二个类的run()应在过几秒后为第一个线程调用modifyAll(),使第一个线程能打印出一条消息。
(5) 在Ticker2内的Counter5.java中,删除yield(),并解释一下结果。用一个sleep()换掉yield(),再解释一下结果。
(6) 在ThreadGroup1.java中,将对sys.suspend()的调用换成对线程组的一个wait()调用,令其等候2秒钟。为了保证获得正确的结果,必须在一个同步块内取得sys的对象锁。
(7) 修改Daemons.java,使main()有一个sleep(),而不是一个readLine()。实验不同的睡眠时间,看看会有什么发生。
(8) 到第7章(中间部分)找到那个GreenhouseControls.java例子,它应该由三个文件构成。在Event.java中,Event类建立在对时间的监视基础上。修改这个Event,使其成为一个线程。然后修改其余的设计,使它们能与新的、以线程为基础的Event正常协作。