14.1 反应灵敏的用户界面

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++));
    }
  }
  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 的计算时间实际却是在所有线程间分摊的。

线程机制多少降低了一些计算效率,但无论程序的设计,资源的均衡,还是用户操作的方便性,都从中获得了巨大的利益。综合考虑,这一机制是非常有价值的。当然,如果本来就安装了多块 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)
Thread 1(4)
Thread 1(3)
Thread 1(2)
Thread 2(5)
Thread 2(4)
Thread 2(3)
Thread 2(2)
Thread 2(1)
Thread 1(1)
All Threads Started
Thread 3(5)
Thread 4(5)
Thread 4(4)
Thread 4(3)
Thread 4(2)
Thread 4(1)
Thread 5(5)
Thread 5(4)
Thread 5(3)
Thread 5(2)
Thread 5(1)
Thread 3(4)
Thread 3(3)
Thread 3(2)
Thread 3(1)

可注意到这个例子中到处都调用了 sleep(),然而输出结果指出每个线程都获得了属于自己的那一部分 CPU 执行时间。从中可以看出,尽管 sleep()依赖一个线程的存在来执行,但却与允许或禁止线程无关。它只不过是另一个不同的方法而已。

亦可看出线程并不是按它们创建时的顺序运行的。事实上,CPU 处理一个现有线程集的顺序是不确定的——除非我们亲自介入,并用 Thread 的 setPriority()方法调整它们的优先级。

main()创建 Thread 对象时,它并未捕获任何一个对象的指针。普通对象对于垃圾收集来说是一种“公平竞赛”,但线程却并非如此。每个线程都会“注册”自己,所以某处实际存在着对它的一个引用。这样一来,垃圾收集器便只好对它“瞠目以对”了。

14.1.2 针对用户界面的多线程

现在,我们也许能用一个线程解决在 Counter1.java 中出现的问题。采用的一个技巧便是在一个线程的 run()方法中放置“子任务”——亦即位于 go()内的循环。一旦用户按下 Start 按钮,线程就会启动,但马上结束线程的创建。这样一来,尽管线程仍在运行,但程序的主要工作却能得以继续(等候并响应用户界面的事件)。下面是具体的代码:

//: Counter2.java
// A responsive user interface with threads
import java.awt.*;
import java.awt.event.*;
import java.applet.*;

class SeparateSubTask extends Thread {
  private int count = 0;
  private Counter2 c2;
  private boolean runFlag = true;
  public SeparateSubTask(Counter2 c2) {
    this.c2 = c2;
    start();
  }
  public void invertFlag() { runFlag = !runFlag;}
  public void run() {
    while (true) {
     try {
      sleep(100);
     } catch (InterruptedException e){}
     if(runFlag)
       c2.t.setText(Integer.toString(count++));
    }
  }
}

public class Counter2 extends Applet {
  TextField t = new TextField(10);
  private SeparateSubTask sp = null;
  private Button
    onOff = new Button("Toggle"),
    start = new Button("Start");
  public void init() {
    add(t);
    start.addActionListener(new StartL());
    add(start);
    onOff.addActionListener(new OnOffL());
    add(onOff);
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(sp == null)
        sp = new SeparateSubTask(Counter2.this);
    }
  }
  class OnOffL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(sp != null)
        sp.invertFlag();
    }
  }
  public static void main(String[] args) {
    Counter2 applet = new Counter2();
    Frame aFrame = new Frame("Counter2");
    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);
  }
} ///:~

现在,Counter2 变成了一个相当直接的程序,它的唯一任务就是设置并管理用户界面。但假若用户现在按下 Start 按钮,却不会真正调用一个方法。此时不是创建类的一个线程,而是创建 SeparateSubTask,然后继续 Counter2 事件循环。注意此时会保存 SeparateSubTask 的指针,以便我们按下 onOff 按钮的时候,能正常地切换位于 SeparateSubTask 内部的 runFlag(运行标志)。随后那个线程便可启动(当它看到标志的时候),然后将自己中止(亦可将 SeparateSubTask 设为一个内部类来达到这一目的)。

SeparateSubTask 类是对 Thread 的一个简单扩展,它带有一个构造器(其中保存了 Counter2 指针,然后通过调用 start()来运行线程)以及一个 run()——本质上包含了 Counter1.java 的 go()内的代码。由于 SeparateSubTask 知道自己容纳了指向一个 Counter2 的指针,所以能够在需要的时候介入,并访问 Counter2 的 TestField(文本字段)。

按下 onOff 按钮,几乎立即能得到正确的响应。当然,这个响应其实并不是“立即”发生的,它毕竟和那种由“中断”驱动的系统不同。只有线程拥有 CPU 的执行时间,并注意到标记已发生改变,计数器才会停止。

  1. 用内部类改善代码

下面说说题外话,请大家注意一下 SeparateSubTask 和 Counter2 类之间发生的结合行为。SeparateSubTask 同 Counter2“亲密”地结合到了一起——它必须持有指向自己“父”Counter2 对象的一个指针,以便自己能回调和操纵它。但两个类并不是真的合并为单独一个类(尽管在下一节中,我们会讲到 Java 确实提供了合并它们的方法),因为它们各自做的是不同的事情,而且是在不同的时间创建的。但不管怎样,它们依然紧密地结合到一起(更准确地说,应该叫“联合”),所以使程序代码多少显得有些笨拙。在这种情况下,一个内部类可以显著改善代码的“可读性”和执行效率:

//: Counter2i.java
// Counter2 using an inner class for the thread
import java.awt.*;
import java.awt.event.*;
import java.applet.*;

public class Counter2i extends Applet {
  private class SeparateSubTask extends Thread {
    int count = 0;
    boolean runFlag = true;
    SeparateSubTask() { start(); }
    public void run() {
      while (true) {
       try {
        sleep(100);
       } catch (InterruptedException e){}
       if(runFlag)
         t.setText(Integer.toString(count++));
      }
    }
  }
  private SeparateSubTask sp = null;
  private TextField t = new TextField(10);
  private Button
    onOff = new Button("Toggle"),
    start = new Button("Start");
  public void init() {
    add(t);
    start.addActionListener(new StartL());
    add(start);
    onOff.addActionListener(new OnOffL());
    add(onOff);
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(sp == null)
        sp = new SeparateSubTask();
    }
  }
  class OnOffL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(sp != null)
        sp.runFlag = !sp.runFlag; // invertFlag();
    }
  }
  public static void main(String[] args) {
    Counter2i applet = new Counter2i();
    Frame aFrame = new Frame("Counter2i");
    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);
  }
} ///:~

这个 SeparateSubTask 名字不会与前例中的 SeparateSubTask 冲突——即使它们都在相同的目录里——因为它已作为一个内部类隐藏起来。大家亦可看到内部类被设为 private(私有)属性,这意味着它的字段和方法都可获得默认的访问权限(run()除外,它必须设为 public,因为它在基础类中是公开的)。除 Counter2i 之外,其他任何方面都不可访问 private 内部类。而且由于两个类紧密结合在一起,所以很容易放宽它们之间的访问限制。在 SeparateSubTask 中,我们可看到 invertFlag()方法已被删去,因为 SeparateSubTask 现在可以直接访问 runFlag。

此外,注意 SeparateSubTask 的构造器已得到了简化——它现在唯一的用外就是启动线程。Counter2i 对象的指针仍象以前那样得以捕获,但不再是通过人工传递和引用外部对象来达到这一目的,此时的内部类机制可以自动照料它。在 run()中,可看到对 t 的访问是直接进行的,似乎它是 SeparateSubTask 的一个字段。父类中的 t 字段现在可以变成 private,因为 SeparateSubTask 能在未获任何特殊许可的前提下自由地访问它——而且无论如何都该尽可能地把字段变成“私有”属性,以防来自类外的某种力量不慎地改变它们。

无论在什么时候,只要注意到类相互之间结合得比较紧密,就可考虑利用内部类来改善代码的编写与维护。

14.1.3 用主类合并线程

在上面的例子中,我们看到线程类(Thread)与程序的主类(Main)是分隔开的。这样做非常合理,而且易于理解。然而,还有另一种方式也是经常要用到的。尽管它不十分明确,但一般都要更简洁一些(这也解释了它为什么十分流行)。通过将主程序类变成一个线程,这种形式可将主程序类与线程类合并到一起。由于对一个 GUI 程序来说,主程序类必须从 Frame 或 Applet 继承,所以必须用一个接口加入额外的功能。这个接口叫作 Runnable,其中包含了与 Thread 一致的基本方法。事实上,Thread 也实现了 Runnable,它只指出有一个 run()方法。

对合并后的程序/线程来说,它的用法不是十分明确。当我们启动程序时,会创建一个 Runnable(可运行的)对象,但不会自行启动线程。线程的启动必须明确进行。下面这个程序向我们演示了这一点,它再现了 Counter2 的功能:

//: Counter3.java
// Using the Runnable interface to turn the
// main class into a thread.
import java.awt.*;
import java.awt.event.*;
import java.applet.*;

public class Counter3
    extends Applet implements Runnable {
  private int count = 0;
  private boolean runFlag = true;
  private Thread selfThread = null;
  private Button
    onOff = new Button("Toggle"),
    start = new Button("Start");
  private TextField t = new TextField(10);
  public void init() {
    add(t);
    start.addActionListener(new StartL());
    add(start);
    onOff.addActionListener(new OnOffL());
    add(onOff);
  }
  public void run() {
    while (true) {
      try {
        selfThread.sleep(100);
      } catch (InterruptedException e){}
      if(runFlag)
        t.setText(Integer.toString(count++));
    }
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(selfThread == null) {
        selfThread = new Thread(Counter3.this);
        selfThread.start();
      }
    }
  }
  class OnOffL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      runFlag = !runFlag;
    }
  }
  public static void main(String[] args) {
    Counter3 applet = new Counter3();
    Frame aFrame = new Frame("Counter3");
    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);
  }
} ///:~

现在 run()位于类内,但它在 init()结束以后仍处在“睡眠”状态。若按下启动按钮,线程便会用多少有些暧昧的表达方式创建(若线程尚不存在):

new Thread(Counter3.this);

若某样东西有一个 Runnable 接口,实际只是意味着它有一个 run()方法,但不存在与之相关的任何特殊东西——它不具有任何天生的线程处理能力,这与那些从 Thread 继承的类是不同的。所以为了从一个 Runnable 对象产生线程,必须单独创建一个线程,并为其传递 Runnable 对象;可为其使用一个特殊的构造器,并令其采用一个 Runnable 作为自己的参数使用。随后便可为那个线程调用 start(),如下所示:

selfThread.start();

它的作用是执行常规初始化操作,然后调用 run()。

Runnable 接口最大的一个优点是所有东西都从属于相同的类。若需访问什么东西,只需简单地访问它即可,不需要涉及一个独立的对象。但为这种便利也是要付出代价的——只可为那个特定的对象运行单独一个线程(尽管可创建那种类型的多个对象,或者在不同的类里创建其他对象)。

注意 Runnable 接口本身并不是造成这一限制的罪魁祸首。它是由于 Runnable 与我们的主类合并造成的,因为每个应用只能主类的一个对象。

14.1.4 制作多个线程

现在考虑一下创建多个不同的线程的问题。我们不可用前面的例子来做到这一点,所以必须倒退回去,利用从 Thread 继承的多个独立类来封装 run()。但这是一种更常规的方案,而且更易理解,所以尽管前例揭示了我们经常都能看到的编码样式,但并不推荐在大多数情况下都那样做,因为它只是稍微复杂一些,而且灵活性稍低一些。

下面这个例子用计数器和切换按钮再现了前面的编码样式。但这一次,一个特定计数器的所有信息(按钮和文本字段)都位于它自己的、从 Thread 继承的对象内。Ticker 中的所有字段都具有 private(私有)属性,这意味着 Ticker 的具体实现方案可根据实际情况任意修改,其中包括修改用于获取和显示信息的数据组件的数量及类型。创建好一个 Ticker 对象以后,构造器便请求一个 AWT 容器(Container)的指针——Ticker 用自己的可视组件填充那个容器。采用这种方式,以后一旦改变了可视组件,使用 Ticker 的代码便不需要另行修改一道。

//: Counter4.java
// If you separate your thread from the main
// class, you can have as many threads as you
// want.
import java.awt.*;
import java.awt.event.*;
import java.applet.*;

class Ticker extends Thread {
  private Button b = new Button("Toggle");
  private TextField t = new TextField(10);
  private int count = 0;
  private boolean runFlag = true;
  public Ticker(Container c) {
    b.addActionListener(new ToggleL());
    Panel p = new Panel();
    p.add(t);
    p.add(b);
    c.add(p);
  }
  class ToggleL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      runFlag = !runFlag;
    }
  }
  public void run() {
    while (true) {
      if(runFlag)
        t.setText(Integer.toString(count++));
       try {
        sleep(100);
      } catch (InterruptedException e){}
    }
  }
}

public class Counter4 extends Applet {
  private Button start = new Button("Start");
  private boolean started = false;
  private Ticker[] s;
  private boolean isApplet = true;
  private int size;
  public void init() {
    // Get parameter "size" from Web page:
    if(isApplet)
      size =
        Integer.parseInt(getParameter("size"));
    s = new Ticker[size];
    for(int i = 0; i < s.length; i++)
      s[i] = new Ticker(this);
    start.addActionListener(new StartL());
    add(start);
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(!started) {
        started = true;
        for(int i = 0; i < s.length; i++)
          s[i].start();
      }
    }
  }
  public static void main(String[] args) {
    Counter4 applet = new Counter4();
    // This isn't an applet, so set the flag and
    // produce the parameter values from args:
    applet.isApplet = false;
    applet.size =
      (args.length == 0 ? 5 :
        Integer.parseInt(args[0]));
    Frame aFrame = new Frame("Counter4");
    aFrame.addWindowListener(
      new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          System.exit(0);
        }
      });
    aFrame.add(applet, BorderLayout.CENTER);
    aFrame.setSize(200, applet.size * 50);
    applet.init();
    applet.start();
    aFrame.setVisible(true);
  }
} ///:~

Ticker 不仅包括了自己的线程处理机制,也提供了控制与显示线程的工具。可按自己的意愿创建任意数量的线程,毋需明确地创建窗口化组件。

在 Counter4 中,有一个名为 s 的 Ticker 对象的数组。为获得最大的灵活性,这个数组的长度是用程序片参数接触 Web 页而初始化的。下面是网页中长度参数大致的样子,它们嵌于对程序片(applet)的描述内容中:

<applet code=Counter4 width=600 height=600>
<param name=size value="20">
</applet>

其中,param,name 和 value 是所有 Web 页都适用的关键字。name 是指程序中对参数的一种引用称谓,value 可以是任何字串(并不仅仅是解析成一个数字的东西)。

我们注意到对数组 s 长度的判断是在 init()内部完成的,它没有作为 s 的内嵌定义的一部分提供。换言之,不可将下述代码作为类定义的一部分使用(应该位于任何方法的外部):

inst size = Integer.parseInt(getParameter("Size"));
Ticker[] s = new Ticker[size]

可把它编译出来,但会在运行期得到一个空指针异常。但若将 getParameter()初始化移入 init(),则可正常工作。程序片框架会进行必要的启动工作,以便在进入 init()前收集好一些参数。

此外,上述代码被同时设置成一个程序片和一个应用(程序)。在它是应用程序的情况下,size 参数可从命令行里提取出来(否则就提供一个默认的值)。

数组的长度建好以后,就可以创建新的 Ticker 对象;作为 Ticker 构造器的一部分,用于每个 Ticker 的按钮和文本字段就会加入程序片。

按下 Start 按钮后,会在整个 Ticker 数组里遍历,并为每个 Ticker 调用 start()。记住,start()会进行必要的线程初始化工作,然后为那个线程调用 run()。

ToggleL 监视器只是简单地切换 Ticker 中的标记,一旦对应线程以后需要修改这个标记,它会作出相应的反应。

这个例子的一个好处是它使我们能够方便地创建由单独子任务构成的大型集合,并以监视它们的行为。在这种情况下,我们会发现随着子任务数量的增多,机器显示出来的数字可能会出现更大的分歧,这是由于为线程提供服务的方式造成的。

亦可试着体验一下 sleep(100)在 Ticker.run()中的重要作用。若删除 sleep(),那么在按下一个切换按钮前,情况仍然会进展良好。按下按钮以后,那个特定的线程就会出现一个 false 的 runFlag,而且 run()会深深地陷入一个无限循环——很难在多任务处理期间中止退出。因此,程序对用户操作的反应灵敏度会大幅度降低。

14.1.5 Daemon 线程

“Daemon”线程的作用是在程序的运行期间于后台提供一种“常规”服务,但它并不属于程序的一个基本部分。因此,一旦所有非 Daemon 线程完成,程序也会中止运行。相反,假若有任何非 Daemon 线程仍在运行(比如还有一个正在运行 main()的线程),则程序的运行不会中止。

通过调用 isDaemon(),可调查一个线程是不是一个 Daemon,而且能用 setDaemon()打开或者关闭一个线程的 Daemon 状态。如果是一个 Daemon 线程,那么它创建的任何线程也会自动具备 Daemon 属性。

下面这个例子演示了 Daemon 线程的用法:

//: Daemons.java
// Daemonic behavior
import java.io.*;

class Daemon extends Thread {
  private static final int SIZE = 10;
  private Thread[] t = new Thread[SIZE];
  public Daemon() {
    setDaemon(true);
    start();
  }
  public void run() {
    for(int i = 0; i < SIZE; i++)
      t[i] = new DaemonSpawn(i);
    for(int i = 0; i < SIZE; i++)
      System.out.println(
        "t[" + i + "].isDaemon() = "
        + t[i].isDaemon());
    while(true)
      yield();
  }
}

class DaemonSpawn extends Thread {
  public DaemonSpawn(int i) {
    System.out.println(
      "DaemonSpawn " + i + " started");
    start();
  }
  public void run() {
    while(true)
      yield();
  }
}

public class Daemons {
  public static void main(String[] args) {
    Thread d = new Daemon();
    System.out.println(
      "d.isDaemon() = " + d.isDaemon());
    // Allow the daemon threads to finish
    // their startup processes:
    BufferedReader stdin =
      new BufferedReader(
        new InputStreamReader(System.in));
    System.out.println("Waiting for CR");
    try {
      stdin.readLine();
    } catch(IOException e) {}
  }
} ///:~

Daemon 线程可将自己的 Daemon 标记设置成“真”,然后产生一系列其他线程,而且认为它们也具有 Daemon 属性。随后,它进入一个无限循环,在其中调用 yield(),放弃对其他进程的控制。在这个程序早期的一个版本中,无限循环会使 int 计数器增值,但会使整个程序都好象陷入停顿状态。换用 yield()后,却可使程序充满“活力”,不会使人产生停滞或反应迟钝的感觉。

一旦 main()完成自己的工作,便没有什么能阻止程序中断运行,因为这里运行的只有 Daemon 线程。所以能看到启动所有 Daemon 线程后显示出来的结果,System.in 也进行了相应的设置,使程序中断前能等待一个回车。如果不进行这样的设置,就只能看到创建 Daemon 线程的一部分结果(试试将 readLine()代码换成不同长度的 sleep()调用,看看会有什么表现)。

下一页