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

本文介绍SwingWorker类的使用方法,演示如何在Swing程序中创建后台线程进行耗时操作,同时保持GUI响应性。文中提供具体示例,展示如何中断线程以及如何在worker线程中获取用户输入。
作者:Hans Muller,Kathy Walrath 
翻译:郭晓刚(foosleeper@163.net)
原文来自java.sun.com

本文给出了一些使用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.  
  2. SwingWorker worker = new SwingWorker() {
  3.    public Object construct() {
  4.       return "Hello" + " " + "World";
  5.    }
  6. };
  7. worker.start();
  8. 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.  
  2. Object doWork() {
  3.    try {
  4.       for(int i = 0; i < NUMLOOPS; i++) {
  5.          updateStatus(i);
  6.          if (Thread.interrupted()) {
  7.             throw new InterruptedException();
  8.          }
  9.          Thread.sleep(500);
  10.       }
  11.    }
  12.    catch (InterruptedException e) {
  13.       updateStatus(0);
  14.       return "Interrupted";  
  15.    }
  16.    return "All Done"
  17. }

在此方法中执行的费时操作应该:周期性地让用户知道它有所进展。updateStatus()方法会为事件派发线程排队Runnable对象(记住:不要在其他线程中执行GUI工作)。一旦按下“Start”按钮,动作监听器(action listener)会创建SwingWorker,使得worker线程被创建。Worker线程启动后,它将执行它的construct()方法,该方法将调用doWork()(如下面的代码所示)。下面的代码例示了Example1实现的SwingWorker的子类。
  1.  
  2. worker = new SwingWorker() {
  3.    public Object construct() {
  4.       return doWork();
  5.    }
  6.    public void finished() {
  7.       startButton.setEnabled(true);
  8.       interruptButton.setEnabled(false);
  9.       statusField.setText(get().toString());
  10.    }
  11. };

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

因为showConfirmDialog()方法弹出一个模态对话框,调用会阻塞直到用户解除该对话框。
为了显示对话框并阻塞调用线程(worker线程)直到对话框被解除,worker线程调用invokeAndWait()方法,定义一个DoShowDialog的实例:
  1.  
  2. DoShowDialog doShowDialog = new DoShowDialog();
  3. try {
  4.    SwingUtilities.invokeAndWait(doShowDialog);
  5. }
  6. catch 
  7.    (java.lang.reflect.
  8.       InvocationTargetException e) {
  9.       e.printStackTrace();
  10. }

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

我们要感谢Doug Lea(《Concurrent Programming in Java》的作者)、Joseph Bowbeer和其他给我们的线程文章和SwingWorker类做出回馈的读者。
-- Hans Muller and Kathy Walrath
### Java Swing线程使用方法及最佳实践 在Java Swing中,为了确保UI的响应性和流畅性,必须正确处理多线程Swing组件不是线程安全的,这意味着不能直接从非事件调度线程(Event Dispatch Thread, EDT)更新UI组件[^1]。以下是一些关键的最佳实践和实现方式: #### 1. 使用`SwingWorker`进行后台任务 `SwingWorker`是专门为Swing设计的类,用于在后台执行耗时任务,同时可以安全地与UI交互。通过重写`doInBackground`方法来定义后台任务,并通过`publish`和`process`方法将中间结果发布到EDT以更新UI。 ```java SwingWorker<Void, Integer> worker = new SwingWorker<Void, Integer>() { @Override protected Void doInBackground() throws Exception { for (int i = 0; i <= 10; i++) { Thread.sleep(1000); // 模拟耗时操作 publish(i); // 发布进度 } return null; } @Override protected void process(List<Integer> chunks) { int progress = chunks.get(chunks.size() - 1); countLabel1.setText(Integer.toString(progress)); // 更新UI } @Override protected void done() { statusLabel.setText("Completed."); // 任务完成后的操作 } }; worker.execute(); ``` 上述代码展示了如何使用`SwingWorker`来执行后台任务并更新UI,同时避免了直接从非EDT线程更新UI的风险[^1]。 #### 2. 使用`SwingUtilities.invokeLater`或`invokeAndWait` 当需要从非EDT线程更新UI时,可以使用`SwingUtilities.invokeLater`或`invokeAndWait`。两者的区别在于:`invokeLater`将任务放入队列后立即返回,而`invokeAndWait`会阻塞当前线程直到任务完成[^5]。 ```java SwingUtilities.invokeLater(new Runnable() { public void run() { countLabel1.setText("Updated Value"); // 安全地更新UI } }); ``` #### 3. 避免直接调用`Thread.run`,改用`Thread.start` 如果在创建`Thread`对象后直接调用`run`方法,实际上是在当前线程执行任务,而不是启动新的线程。正确的做法是调用`start`方法来启动新线程[^4]。 ```java Thread worker = new Thread(() -> { try { Thread.sleep(1000); // 模拟耗时操作 } catch (InterruptedException e) { e.printStackTrace(); } SwingUtilities.invokeLater(() -> { countLabel1.setText("Task Completed"); }); }); worker.start(); // 正确启动新线程 ``` #### 4. 使用计时器(`javax.swing.Timer`) 对于定期更新UI的任务,可以使用`javax.swing.Timer`。它会在EDT上执行任务,因此可以直接更新UI组件[^3]。 ```java import javax.swing.Timer; Timer timer = new Timer(1000, e -> { int count = Integer.parseInt(countLabel1.getText()); countLabel1.setText(String.valueOf(count + 1)); if (count >= 10) { ((Timer) e.getSource()).stop(); // 停止计时器 } }); timer.start(); ``` #### 5. 避免UI阻塞的最佳实践 - **不要在EDT中执行耗时操作**:所有耗时任务应移至后台线程- **及时释放资源**:在多线程环境中,确保正确关闭连接、释放锁等资源[^4]。 - **异常处理**:在后台线程中捕获并处理异常,避免程序崩溃。 ### 示例代码 以下是一个完整的示例,展示如何结合`SwingWorker`和`SwingUtilities.invokeLater`实现多线程任务: ```java import javax.swing.*; import java.awt.*; import java.util.List; public class SwingMultithreadingExample { private JLabel countLabel1 = new JLabel("0"); private JLabel statusLabel = new JLabel("Running..."); public SwingMultithreadingExample() { JFrame frame = new JFrame("Swing Multithreading Example"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLayout(new FlowLayout()); frame.add(new JLabel("Count: ")); frame.add(countLabel1); frame.add(statusLabel); frame.setSize(300, 100); frame.setVisible(true); startBackgroundTask(); } private void startBackgroundTask() { SwingWorker<Void, Integer> worker = new SwingWorker<Void, Integer>() { @Override protected Void doInBackground() throws Exception { for (int i = 0; i <= 10; i++) { Thread.sleep(1000); publish(i); } return null; } @Override protected void process(List<Integer> chunks) { int progress = chunks.get(chunks.size() - 1); countLabel1.setText(Integer.toString(progress)); } @Override protected void done() { statusLabel.setText("Completed."); } }; worker.execute(); } public static void main(String[] args) { SwingUtilities.invokeLater(SwingMultithreadingExample::new); } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值