`

使用Swing Worker线程 --执行后台任务的新方法

阅读更多

本文给出了一些使用SwingWorker类的例子。SwingWorker类的目的是实现一个后台线程,让你可以用它来执行一些费时的操作,而不影响你的程序的GUI的性能。关于SwingWorker类的一些基本信息,请参阅《线程和Swing》。
注意:在2000年9月我们修改了这篇文章和它的例子以适用于一个更新版本的SwingWorker类。SwingWorker类的这个版本修正了一些微妙的线程bug。
对执行一些费时或可阻塞操作的Swing程序来说,线程是基本的解决之道。例如,如果你的应用程序要根据用户选择的菜单项发出一个数据库请求或加载一个文件,那么你应该在一个单独的线程中完成这些工作。本文阐述了在一个分离的worker线程中完成上述工作的途径。
本文包括以下主要内容:

  • SwingWorker类:这一部分告诉你怎样下载SwingWorker类并描述了SwingWorker类的用途。介绍了SwingWorker类的interrupt()方法。
  • 引入Worker线程的例子:演示一个运用SwingWorker类的应用程序。
  • 例一:中断一个Swing Worker线程:解释如何运用interrupt()方法来中断worker线程。
  • 例二:从Swing Worker线程反馈给用户:例一的增强,添加了一个模式对话框以提示用户输入。


概览:SwingWorker类


因为SwingWorker类并不是Java发行版的一部分,你需要下载和编译它才能使用。它的源代码在这里:
SwingWorker.java

SwingWorker类简介


SwingWorker类可以简单且方便地用于在一个新的线程中计算一个数值。要使用这个类,你只要创建一个SwingWorker的子类并覆盖SwingWorker.construct()方法来执行计算。然后你实例化它,并在这个新实例上调用start()方法。
例如,下面的代码片断中产生了一个线程,其中构造了一个字符串。然后,片断中使用了get()方法来取得前面由construct方法所返回的值,并且在必要时将等待。最后,在显示器上显示出字符串的值。

  1. SwingWorker worker = new SwingWorker() {
  2.    public Object construct() {
  3.       return "Hello" + " " + "World";
  4.    }
  5. };
  6. worker.start();
  7. System.out.println(worker.get().toString());


在实际的应用程序中,construct方法会做些更有用(但可能很费时)的事情。比如,它可能做下列工作之一:

  • 执行大量的运算
  • 执行可能导致大量的类被装载的代码
  • 为网络或磁盘I/O阻塞
  • 等待其他资源


在上面的代码片断中没有展示的一个SwingWorker类的特性是,当construct()返回后,SwingWorker可以让你在事件派发线程中执行一些代码。你可以通过覆盖SwingWorker的finished()方法来做到这一点。典型地,你可以用finished()来显示刚刚构造的一些组件或设置组件上显示的数据。
原始版本的SwingWorker类的一个局限是,一旦一个worker线程开始运行,你无法中断它。(译注:本文、本文的例子及SwingWorker类曾被更新。)不管怎么说,对于一个交互应用程序来说,让用户在工作线程完成前一直等待是相当糟糕的风格。如果用户希望终止一项正在执行中的操作,执行此操作的线程应该能够尽快中止。

使用interrupt()方法


在第二版的SwingWorker类中加入了一个interrupt()方法以允许中断一个线程。你的线程应该以下面两种途径之一得到中断的通知:

  • 正在执行诸如sleep()或wait()方法的线程在interrupt()被调用时会抛出一个InterruptedException。
  • 线程可以显式询问它是否已被中断,通过形如以下代码:
    1. Thread.interrupted()
     


使用sleep()或wait()方法的工作线程(如后面例子中的线程)一般不需要显式检查是否被中断。通常让sleep()或wait()方法抛出InterruptedException就足够了。
不过,如果你希望能够中断一个不含定时循环的SwingWorker,还是需要用interrupted()方法来显式检查中断。

引入Worker线程的例子


本文余下的部分讨论一个包含两个worker线程的例子的程序。下图是程序主窗口的截图,和它弹出的一个对话框:


例子的源代码由以下文件组成:


你可以下载一个包含上面所有文件的zip文件。在此zip文件中还包含一个带有HTML格式的说明的例子,这个版本的例子更能说明问题(但也更复杂)。
运行此例子前要先编译:
/usr/local/java/jdk1.3.0/bin/javac ThreadsExample.java
用以下命令行运行此例子:
/usr/local/java/jdk1.3.0/bin/java ThreadsExample
当你按下“Start”按钮,相应例子的worker线程将被创建。你可以在进度条中查看它的进度。你可以按下“Cancel”按钮来中断worker线程。在启动例子二稍等几秒,你会看到一个对话框提示你确认是否执行。稍后我们再详述这个。

例子一:中断Swing Worker线程


接着我们要讨论的例子是Example1.java。这个例子中的worker线程包含一个执行100次的循环,并在两次循环之间睡眠半秒。

  1. //progressBar maximum is NUMLOOPS (100) progressBar的最大值是NUMLOOPS (100)
  2. for(int i = 0; i < NUMLOOPS; i++) {
  3.    updateStatus(i);
  4.    ...
  5.    Thread.sleep(500);
  6. }


为了向你展示如何使用interrupt()方法,我们的例子程序可以让你启动一个SwingWorker然后等待它完成或者中断它。程序中的SwingWorker子类的construct()方法所作的唯一一件事就是调用Example1的doWork()方法。doWork()方法的完整源码列在下面。这个例子在处理worker线程时会重置进度条并把标签设为“Interrupted”。因为中断可能发生在sleep()方法调用之外,所以代码中在调用sleep()方法之前要先检查中断与否。

  1. Object doWork() {
  2.    try {
  3.       for(int i = 0; i < NUMLOOPS; i++) {
  4.          updateStatus(i);
  5.          if (Thread.interrupted()) {
  6.             throw new InterruptedException();
  7.          }
  8.          Thread.sleep(500);
  9.       }
  10.    }
  11.    catch (InterruptedException e) {
  12.       updateStatus(0);
  13.       return "Interrupted";  
  14.    }
  15.    return "All Done"
  16. }


在此方法中执行的费时操作应该:周期性地让用户知道它有所进展。updateStatus()方法会为事件派发线程排队Runnable对象(记住:不要在其他线程中执行GUI工作)。一旦按下“Start”按钮,动作监听器(action listener)会创建SwingWorker,使得worker线程被创建。Worker线程启动后,它将执行它的construct()方法,该方法将调用doWork()(如下面的代码所示)。下面的代码例示了Example1实现的SwingWorker的子类。

  1. worker = new SwingWorker() {
  2.    public Object construct() {
  3.       return doWork();
  4.    }
  5.    public void finished() {
  6.       startButton.setEnabled(true);
  7.       interruptButton.setEnabled(false);
  8.       statusField.setText(get().toString());
  9.    }
  10. };


finished()方法在construct()方法返回后执行(即worker线程完成后)。它的任务只是简单地重新使“Start”按钮有效,同时使“Cancel”按钮无效,并将状态域显示的值设置成worker的计算结果。记住finished()方法是在事件派发线程中执行的,所以它可以安全地直接更新GUI。

例子二:从Worker线程提示用户


这个例子实现为Example1的子类。唯一的区别是,在worker线程执行了大约两秒后,它将阻塞直到用户响应一个Continue/Cancel模态对话框。如果用户选择的不是“Continue”,我们就退出doWork()循环。
这个例子演示了一个在许多worker线程中应用的惯用法:如果worker执行中到达一个不期望的状态,它将阻塞起来直到用户被提醒或用一个模态对话框从用户那里收集到了更多信息。这种做法有一点复杂,因为对话框的显示需要放到事件派发线程中,且worker线程需要被阻塞直到用户解除了该模式对话框。
我们使用SwingUtilities的invokeAndWait()方法来在事件派发线程中弹出对话框。与invokeLater()不同,invokeAndWait()会阻塞起来直到它的Runnable对象返回。在我们的例子中,Runnable对象直到对话框被解除才返回。我们创建一个内部Runnable类DoShowDialog,来完成弹出对话框。一个实例变量DoShowDialog.proceedConfirmed,被用来记录用户的选择:

  1. class DoShowDialog implements Runnable {
  2.    boolean proceedConfirmed;
  3.    public void run() {
  4.       Object[] options = {"Continue""Cancel"};
  5.          int n = JOptionPane.showOptionDialog
  6.          (Example2.this,
  7.          "Example2: Continue?",
  8.          "Example2",
  9.          JOptionPane.YES_NO_OPTION,
  10.          OptionPane.QUESTION_MESSAGE,
  11.             null,
  12.             options,
  13.             "Continue");
  14.          proceedConfirmed =
  15.             (n == JOptionPane.YES_OPTION);
  16.    }
  17. }


因为showConfirmDialog()方法弹出一个模态对话框,调用会阻塞直到用户解除该对话框。
为了显示对话框并阻塞调用线程(worker线程)直到对话框被解除,worker线程调用invokeAndWait()方法,定义一个DoShowDialog的实例:

  1. DoShowDialog doShowDialog = new DoShowDialog();
  2. try {
  3.    SwingUtilities.invokeAndWait(doShowDialog);
  4. }
  5. catch 
  6.    (java.lang.reflect.
  7.       InvocationTargetException e) {
  8.       e.printStackTrace();
  9. }


代码中捕获的InvocationTargetException是调试DoShowDialog的run()方法的残留。当invokeAndWait()方法返回后,worker线程可以读取doShowDialog.proceedConfirmed来获得用户的响应。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics