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吗?

象这样的一些问题是造成多线程应用程序的编制成为一种“技术活”的原因之一。