Swing线程--工作者线程和SwingWorker

一. 初始化线程

每个程序都有一组线程作为应用程序逻辑开始的地方。在标准的程序中,只有一个这样的线程:这个线程调用程序类的main方法。在Applet初始化线程中有一个创建Applet对象以及调用Appletinitstart方法的线程;这些动作可能发生在单一线程上,也可能是两个或更多的不同线程上,这依赖与Java平台的具体实现。在这个教程中,我们称这些线程为初始化线程。

Swing程序中,初始化线程不能有太多的事情做。他们最基本的工作就是去创建一个Runnable对象,这个Runnable对象用于初始化GUI以及在事件调度线程上执行对象的计划任务。一旦GUI被创建,主要通过GUI事件来驱动程序,所以应该在事件调度线程上引发每一个短任务的执行。应用程序代码可以在事件调度线程上完成附加的任务(如果他们完成的很快,那么就不需要用户界面上的事件处理)或者在一个工作者线程上(如果是长时间的任务)

一个初始化线程通过调用javax.swing.SwingUtilities.invokeLater方法或者javax.swing.SwingUtilities.invokeAndWait方法完成GUI的创建任务。这两个方法都带有单一的参数:一个定义新任务的Runnable对象。他们仅仅通过名称来显示他们之间的区别:invokeLater简单的完成任务并返回;invokeAndWait则在返回之前必需等待任务执行完成。

你可以看这个贯穿Swing教程的例子:

SwingUtilities.invokeLater(new Runnable() {

public void run() {

createAndShowGUI();

}

}

applet中,GUI的创建任务必需从使用invokeAndWaitinit方法中开始。否则,init方法可能在GUI被创建之前就返回了,这可能在web浏览器开始一个applet时引发问题。在一些其他类型的程序中,完成GUI创建的任务通常是初始化线程最后要做的事情,因此使用invokeLater还是invokeAndWait就变得无关紧要了。

为什么不在初始化线程本身中简单的创建GUI呢?因为几乎所有创建Swing组件或者与Swing组件有关系的代码都必需运行在事件调度线程上。这个约束的讨论将在下一节中进行。

二. 事件调度线程

Swing事件处理代码运行在一个特殊的线程上,就是通常所说的事件调度线程。大多数调用Swing方法的代码同样运行在这个线程上。这是必需的因为大多数Swing对象的方法并非“线程安全”:从多个线程中调用他们要冒着线程冲突或者内存不一致错误的风险。一些Swing组件方法在API规范中被标注为“线程安全”;这些组件方法可以从任何线程上被安全的调用。而所有其他的Swing组件方法必需从事件调度线程上被调用。虽然程序可能时常运行正常而忽略这个规则,但是易受到不可预知错误的影响而难以再用。

关于线程安全的说明:这种看似陌生而又是Java平台并非线程安全的一个重要方面。原来任何尝试创建一个线程安全的GUI类库都面临着一些根本的问题。关于这个问题的更多信息,请查看以下的关于Graham Hamiltonblog:多线程工具包:一个失败的梦想?

将一系列的短任务代码运行于事件调度线程上是一个很好的想法。大多数任务被事件处理方法调用,例如ActionListener.actionPerformed。利用invokeLater或者invokeAndWait方法可以通过应用程序代码来完成其他的任务。在事件调度线程上的任务必需被很快的完成;如果他们不是,那么未被处理的事件会倒退并且用户界面变的反应迟钝。

如果你需要测定你的代码是否运行在事件调度线程上,请调用javax.swing.SwingUtilities.isEventDispatchThread来进行测试。

三. 工作者线程和SwingWorker

当一个Swing程序需要执行一个长时间任务时,它通常使用工作者线程之一,也就是通常所说的后台线程。每个运行于工作者线程上的任务通过javax.swing.SwingWorker的实例来表现。SwingWorker本身是一个抽象类;为了创建一个SwingWorker对象你必需定义一个子类;匿名内部类常常具有创建非常简单的SwingWorker对象的用途。

SwingWorker提供了许多通讯和控制特性:

l SwingWorker的子类可以定义一个方法,done方法,这个方法在后台任务完成时自动的在事件调度线程上被调用。

l SwingWorker实现了java.util.concurrent.Future。这个接口允许后台任务提供一个返回值给另外一个线程。这个接口中的其他方法允许后台任务的取消以及探查后台任务是否已被完成或者已被取消。

l 后台任务可以通过调用SwingWorker.publish方法提供中间计算结果,并从事件调度线程上引起SwingWorker.process方法的调用。

l 后台任务可以定义约束属性。更改这些属性将触发事件,并从事件调度线程上引起事件处理方法的调用。

这些特性将在以下的子章节中被讨论。

注意:javax.swing.SwingWorker类被加入到Java SE 6平台中。在较早的版本中,另外一个类,同样被叫做SwingWorker,已经被广泛的用于一些相同的目的。老版本的SwingWorker并非Java平台规范的一部分,因此没有在JDK部分被提供。

新版本的SwingWorker是一个完整的新类。它的功能并非是老版本SwingWorker的一个严格意义上的扩展集。在这两个类中的方法虽然实现了相同的功能,但是并非拥有相同的名称。当javax.swing.SwingWorker的一个新实例需要各自的新后台任务时,老版本的SwingWorker类实例同样可被重复使用。

在整个Java指南中,任何一个关于SwingWorker的讨论现在只针对javax.swing.SwingWorker

1. 简单的后台任务

让我们以一个任务开始,这个任务虽然非常的简单,但是非常的耗费时间。TumbleItem小应用程序载入一组图形文件并将其用于一个动画中。如果从一个初始化线程中载入图形文件的话,那么在GUI出现之前可能会被阻塞。如果从一个事件调度线程中载入这些图形文件的话,那么GUI可能会暂时的失去响应。

为了避免这些问题,TumbleItem对象从它的初始化线程中创建和执行一个SwingWorker实例对象。该对象的doInBackground方法执行于一个工作者线程上,它载入图片并放入到一个ImageIcon类型的数组中,并且返回一个该数组的引用。然后done方法在事件调度线程中被执行,它调用get方法接收这个数组的引用,并赋值给applet类中的一个名叫imgs的属性,这个属性允许TumbleItem马上构造一个GUI,而不用等待图片加载的完成。

这里的代码定义和执行了SwingWorker对象。

SwingWorker worker = new SwingWorker<ImageIcon[], Void>() {

@Override

public ImageIcon[] doInBackground() {

final ImageIcon[] innerImgs = new ImageIcon[nimgs];

for (int i = 0; i < nimgs; i++) {

innerImgs[i] = loadImage(i+1);

}

return innerImgs;

}

@Override

public void done() {

//Remove the "Loading images" label.

animator.removeAll();

loopslot = -1;

try {

imgs = get();

} catch (InterruptedException ignore) {}

catch (java.util.concurrent.ExecutionException e) {

String why = null;

Throwable cause = e.getCause();

if (cause != null) {

why = cause.getMessage();

} else {

why = e.getMessage();

}

System.err.println("Error retrieving file: " + why);

}

}

};

所有具体的SwingWorker子类必需实现doInBackground方法,而done方法的实现是可选的。

注意,SwingWorker是一个泛型类,带有2个类型参数。第一个类型参数为doInBackground方法指定一个返回类型,并且也为get方法指定一个返回类型,通过调用另外一个线程来接收由doInBackground方法返回的对象。当后台任务仍旧处于激活状态时,SwingWorker的第二个类型参数为中间计算结果指定一个返回类型。因为这个例子无需返回中间计算结果,Void可以被用来作为一个占位符。

你可能想知道设置imgs属性的代码是不是非得这样复杂。为什么要使doInBackground返回一个对象并且在done方法中接收它?为何不在刚才的doInBackground方法中直接设置imgs属性?这个问题的关键在于imgs对象在工作者线程中被创建并且在事件调度线程中被使用(译者注:工作在不同的线程中)。当对象通过这种方式在线程之间被共享时,你必需确保在一个线程中的更改可以被另外一个线程所访问。利用get方法可以保证这点,因为利用get方法可以创建代码之间的一种happen-before关系(译者注:happen-before关系是一种操作内存读写共享变量的锁机制,这种机制可以保证一个线程写入的结果对另一个线程的读取是可视的,详情请参考Java语言规范的第17),代码之间的这种关系可以创建imgs对象并且代码可以使用它。

关于happen-before关系的更多信息,请参考在Concurrency教程中的内存一致性错误

实际上有两个方法用于接收由doInBackground方法返回的对象。

l 调用不带参数的SwingWorker.get方法。如果后台任务没有完成,则get方法将阻塞直到后台任务完成。

l 调用带指定超时时间参数的SwingWorker.get方法。如果后台任务没有完成,则get方法将阻塞直到后台任务完成——除非超时首先过期,在这样的情况下,get方法抛出java.util.concurrent.TimeoutException异常。

在从事件调度线程中调用任何一个get的重载方法时都要当心,直到get方法返回,否则没有GUI事件将被处理并且GUI是“冻结”的。不要调用不带参数的get方法,除非你确信后台任务是完成的或者接近于完成。

关于TumbleItem例子的更多信息,请参考在使用其他Swing特性教程中的如何使用Swing计时器章节。

2. 有中间计算结果的任务

当后台任务仍旧处于工作状态时,它经常具有提供中间计算结果的用途。通过调用SwingWorker.publish方法,后台任务就可以实现这个功能。这个方法接受一个变长数目的参数。每个参数的类型必需由SwingWorker的第二个类型参数指定。

覆盖SwingWorker.process方法来收集由publish方法提供的计算结果。这个方法将从事件调度线程上被调用。对publish方法的多次调用所得到的计算结果经常被堆积在一个单一的process方法调用中。

让我们来看一个使用publish方法提供中间计算结果的Flipper例子。这个程序在后台任务中通过产生一系列的随机布尔值来测试java.util.Random类的公正性。这相当于抛硬币;因此我们称它Flipper。后台任务使用一个FlipPair类型的对象来报告它的计算结果。

private static class FlipPair {

private final long heads, total;

FlipPair(long heads, long total) {

this.heads = heads;

this.total = total;

}

}

heads属性表示随机布尔值为true的次数数目;total属性表示随机布尔值的总数目。

后台任务是通过FlipTask实例来表现:

private class FlipTask extends SwingWorker<Void, FlipPair> {

由于后台任务并非返回一个最终结果,所以第一个类型参数就变得无关紧要;Void被用来作为一个占位符。在每一次“抛硬币”之后,后台任务就调用publish方法。

@Override

protected Void doInBackground() {

long heads = 0;

long total = 0;

Random random = new Random();

while (!isCancelled()) {

total++;

if (random.nextBoolean()) {

heads++;

}

publish(new FlipPair(heads, total));

}

return null;

}

(isCancelled方法将在下一章节中讨论)因为publish方法被频繁的调用,所以在事件调度线程中大量的FlipPair对象可能被堆积在process方法被调用之前;process方法每次只报告最后一次的值,并用它来更新GUI界面:

protected void process(List pairs) {

FlipPair pair = pairs.get(pairs.size() - 1);

headsText.setText(String.format("%d", pair.heads));

totalText.setText(String.format("%d", pair.total));

devText.setText(String.format("%.10g",

((double) pair.heads)/((double) pair.total) - 0.5));

}

如果随机布尔值是公正的,那么当Flipper在运行的时候,在devText中显示的值应该越来越接近于0

注释:setText方法在Flipper中的使用实际上是“线程安全”的,就象在它的规范中定义的那样。这意味着我们可以省略掉publishprocess的处理方式而从工作者线程上直接设置文本域的值。为了提供一个简单的SwingWorker中间计算结果的示范,我们选择了忽略这个事实。

3. 取消执行中的后台任务

为了取消一个正在运行的后台任务,可以调用SwingWorker.cancel方法。后台任务必需与它自己的取消合作。这里有两种方法可以完成这件事情:

l 当它接收到一个中断时就终止。这个过程在Concurrency中的Interrupts章节中被描述。

l 常常通过调用SwingWorker.isCanceled方法。如果这个SwingWorker对象的取消已经被调用,那么这个方法返回true

取消方法带有一个单一的布尔参数。如果这个参数为true,那么cancel方法发送一个中断给后台任务。无论参数是true还是false,调用cancel方法将把对象的取消状态设置为true。这个值可以通过isCanceled方法返回。一旦改变发生,取消状态将不能被改回。

上一小节的Flipper例子使用了“status-only 习惯用语(译者注:status-only可能理解为在方法中仅仅处理对象的状态,这里的方法当然指doInBackground方法)。当isCancelled方法返回true时,将从doInBackground方法中退出主循环。这将发生在当用户点击“Cancel”按钮的时候,此时触发的代码将会调用带有一个false参数的cancel方法。

Flipper例子使用status-only方法是明知的,因为SwingWorker.doInBackground方法的实现不包括任何的可能抛出InterruptedException异常的代码。为了对一个中断有回应,后台任务将不得不常常的调用Thread.isInterrupted方法。为了得到相同的目的也可以同样方便的使用SwingWorker.isCancelled方法。

注释:如果在一个SwingWorker对象的后台任务已经被取消之后再去调用get方法,那么java.util.concurrent.CancellationException异常将被抛出。

4. 约束属性和状态的方法

SwingWorker支持约束属性,这个功能具有与其他线程通讯的用途。有两个约束属性被预先定义:progressstate。与使用所有约束属性一样,progressstate可以被用来在事件调度线程上触发事件处理任务。

通过实现一个PropertyChangeListener监听器,程序可以用progress state和其他约束属性来跟踪属性的变化。要了解更多的信息,请参考在编写事件监听器中的编写PropertyChangeListener监听器章节。

progress 约束变量(The progress Bound Variable

progress 约束变量可以是一个从0100的整型值。它有一个预先定义的设置器方法(protected SwingWorker.setProgress)和一个预先定义的获取器方法(public SwingWorker.getProgress)。

ProgressBarDemo例子从一个后台任务中使用progress属性来更新一个进度条控件。关于这个例子的详细讨论,请参考在使用Swing组件中的如何使用进度条章节。

state约束变量(The state Bound Variable

state约束变量显示了SwingWorker在它的生命周期中所处的阶段。这个约束变量包含一个SwingWorker.StateValue类型的枚举值。可能的值为:

PENDING

从(SwingWorker)对象的创建直到doInBackground方法刚刚被调用之前的这段时间内的状态。

STARTED

doInBackground方法被调用之前不久直到done方法被调用之前不久的这段时间内的状态。

DONE

SwingWorker)对象存在的其余状态(译者注:也就是done方法被调用之前不久之后)

当前state约束变量的值可以通过SwingWorker.getState方法返回。

状态方法(Status Methods

2个方法可以报告后台任务的当前状态,这2个方法是Future接口的一部分。正如你在取消后台任务中看到的,如果后台任务已经被取消,那么isCancelled方法返回true。另外,如果后台任务已经被完成,那么isDone方法返回true,因此要么是正常状态,要么是正在被取消状态。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值