# 14.5 回顾runnable
在本章早些时候,我曾建议大家在将一个程序片或主`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机制在速度上造成的戏剧性的差异。
## 14.5.1 过多的线程
有些时候,我们会发现`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()`,令其挑选一种新的随机颜色。
运行这个程序时,大家会发现它确实变得更快,响应也更迅速(比如在中断它的时候,它能更快地停下来)。而且随着网格尺寸的壮大,它也不会经常性地陷于“停顿”状态。因此,线程的处理又多了一项新的考虑因素:必须随时检查自己有没有“太多的线程”(无论对什么程序和运行平台)。若线程太多,必须试着使用上面介绍的技术,对程序中的线程数量进行“平衡”。如果在一个多线程的程序中遇到了性能上的问题,那么现在有许多因素需要检查:
(1) 对`sleep`,`yield()`以及/或者`wait()`的调用足够多吗?
(2) `sleep()`的调用时间足够长吗?
(3) 运行的线程数是不是太多?
(4) 试过不同的平台和JVM吗?
象这样的一些问题是造成多线程应用程序的编制成为一种“技术活”的原因之一。
- Java 编程思想
- 写在前面的话
- 引言
- 第1章 对象入门
- 1.1 抽象的进步
- 1.2 对象的接口
- 1.3 实现方案的隐藏
- 1.4 方案的重复使用
- 1.5 继承:重新使用接口
- 1.6 多态对象的互换使用
- 1.7 对象的创建和存在时间
- 1.8 异常控制:解决错误
- 1.9 多线程
- 1.10 永久性
- 1.11 Java和因特网
- 1.12 分析和设计
- 1.13 Java还是C++
- 第2章 一切都是对象
- 2.1 用引用操纵对象
- 2.2 所有对象都必须创建
- 2.3 绝对不要清除对象
- 2.4 新建数据类型:类
- 2.5 方法、参数和返回值
- 2.6 构建Java程序
- 2.7 我们的第一个Java程序
- 2.8 注释和嵌入文档
- 2.9 编码样式
- 2.10 总结
- 2.11 练习
- 第3章 控制程序流程
- 3.1 使用Java运算符
- 3.2 执行控制
- 3.3 总结
- 3.4 练习
- 第4章 初始化和清除
- 4.1 用构造器自动初始化
- 4.2 方法重载
- 4.3 清除:收尾和垃圾收集
- 4.4 成员初始化
- 4.5 数组初始化
- 4.6 总结
- 4.7 练习
- 第5章 隐藏实现过程
- 5.1 包:库单元
- 5.2 Java访问指示符
- 5.3 接口与实现
- 5.4 类访问
- 5.5 总结
- 5.6 练习
- 第6章 类复用
- 6.1 组合的语法
- 6.2 继承的语法
- 6.3 组合与继承的结合
- 6.4 到底选择组合还是继承
- 6.5 protected
- 6.6 累积开发
- 6.7 向上转换
- 6.8 final关键字
- 6.9 初始化和类装载
- 6.10 总结
- 6.11 练习
- 第7章 多态性
- 7.1 向上转换
- 7.2 深入理解
- 7.3 覆盖与重载
- 7.4 抽象类和方法
- 7.5 接口
- 7.6 内部类
- 7.7 构造器和多态性
- 7.8 通过继承进行设计
- 7.9 总结
- 7.10 练习
- 第8章 对象的容纳
- 8.1 数组
- 8.2 集合
- 8.3 枚举器(迭代器)
- 8.4 集合的类型
- 8.5 排序
- 8.6 通用集合库
- 8.7 新集合
- 8.8 总结
- 8.9 练习
- 第9章 异常差错控制
- 9.1 基本异常
- 9.2 异常的捕获
- 9.3 标准Java异常
- 9.4 创建自己的异常
- 9.5 异常的限制
- 9.6 用finally清除
- 9.7 构造器
- 9.8 异常匹配
- 9.9 总结
- 9.10 练习
- 第10章 Java IO系统
- 10.1 输入和输出
- 10.2 增添属性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File类
- 10.5 IO流的典型应用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 压缩
- 10.9 对象序列化
- 10.10 总结
- 10.11 练习
- 第11章 运行期类型识别
- 11.1 对RTTI的需要
- 11.2 RTTI语法
- 11.3 反射:运行期类信息
- 11.4 总结
- 11.5 练习
- 第12章 传递和返回对象
- 12.1 传递引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只读类
- 12.5 总结
- 12.6 练习
- 第13章 创建窗口和程序片
- 13.1 为何要用AWT?
- 13.2 基本程序片
- 13.3 制作按钮
- 13.4 捕获事件
- 13.5 文本字段
- 13.6 文本区域
- 13.7 标签
- 13.8 复选框
- 13.9 单选钮
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 视窗化应用
- 13.16 新型AWT
- 13.17 Java 1.1用户接口API
- 13.18 可视编程和Beans
- 13.19 Swing入门
- 13.20 总结
- 13.21 练习
- 第14章 多线程
- 14.1 反应灵敏的用户界面
- 14.2 共享有限的资源
- 14.3 堵塞
- 14.4 优先级
- 14.5 回顾runnable
- 14.6 总结
- 14.7 练习
- 第15章 网络编程
- 15.1 机器的标识
- 15.2 套接字
- 15.3 服务多个客户
- 15.4 数据报
- 15.5 一个Web应用
- 15.6 Java与CGI的沟通
- 15.7 用JDBC连接数据库
- 15.8 远程方法
- 15.9 总结
- 15.10 练习
- 第16章 设计模式
- 16.1 模式的概念
- 16.2 观察器模式
- 16.3 模拟垃圾回收站
- 16.4 改进设计
- 16.5 抽象的应用
- 16.6 多重分发
- 16.7 访问器模式
- 16.8 RTTI真的有害吗
- 16.9 总结
- 16.10 练习
- 第17章 项目
- 17.1 文字处理
- 17.2 方法查找工具
- 17.3 复杂性理论
- 17.4 总结
- 17.5 练习
- 附录A 使用非JAVA代码
- 附录B 对比C++和Java
- 附录C Java编程规则
- 附录D 性能
- 附录E 关于垃圾收集的一些话
- 附录F 推荐读物