本文示例代码请从这儿下载
正确理解和使用Swing线程模型编程是编写响应灵活的Swing程序的关键。从Java SE 6开始引进的SwingWorker能帮你轻松的编写多线程Swing程序,改善你Swing程序的结构,提高界面响应的灵活性。SDN(Sun developer Network)上有一篇很好的文章:Improve Application Performance With SwingWorker in Java SE 6详细演示了如何使用SwingWorker改善Swing应用程序。把它翻译过来同大家共享。
摘要
桌面应用程序员常见的错误是误用Swing事件调度线程(Event Dispatch Thread, EDT)。他们要么从非UI线程访问UI组件,要么不考虑事件执行顺序,要么不使用独立任务线程而在EDT线程上执行耗时任务,结果使编写的应用程序变得响应迟钝、速度很慢。耗时计算和输入/输出(IO)密集型任务不应放在Swing EDT上运行。发现这种问题的代码并不容易,但Java SE 6提供了javax.swing.SwingWorker类,使修正这种代码变得更容易。
本文演示了一个使用SwingWorker类创建和管理任务线程的例子,描述了如何避免编写运行缓慢、感觉迟钝、容易失去响应的用户界面。这个演示例子叫Image Search,它展示了如何使用SwingWorker API来和网站Flickr进行交互、搜索并下载图像。
如果需要理解Swing Ui的基本概念,包括事件处理和侦听等UI编程,可以参照前面的文章,或从Sun官方网站下载阅读Java教程的Swing部分。
演示程序介绍
Image Search执行的耗时任务是访问Flickr网站服务,该任务不应该在EDT上执行。Image Search程序搜索Flickr站点,搜索匹配用户输入的查询条件的图像,下载匹配图形的缩略图。当用户从缩略图列表中选择某缩略图时,它将下载该图的原始图片。该演示程序使用SwingWorker类作为任务线程,从而避免了在EDT上执行这些耗时任务。
当用户输入查询条件时,程序在Flickr网站请求一个图像查询。如果有符合查询条件的图像,程序下载上限为100个的缩略图像。可以修改程序改变下载图像的数目。搜索和下载图像的同时,有一进度条显示搜索进度。图1显示了查询字段和进度条:
图1,搜索图像并显示下载进度
每当程序成功下载一个缩略图片后,就添加到一个JList组件中,图片从Flickr站点到达后就被添加列表中。程序使用SwingWorker的一个实例,程序能在每个图片到达时添加到列表,而不用等待所有的图片都到达。图2显示列表中的图片:
图2匹配缩略图的列表
当从列表选择一个图片,程序将下载该图片的原始图片,并显示在列表的下面。当大图片下载时,另一进度条将显示下载进度。图3显示列表和图片下载进度条。
图3选中缩略图下载大图片
最后,当所有图片数据下载完毕后,程序在列表下方显示图片。
程序使用SwingWorker来完成所有图片搜索和下载任务。另外,程序还演示了如何取消任务,如何在任务完成之前获得即时结果。该程序有两个SwingWorker的子类:ImageSearcher和ImageRetriever。ImageSearcher类负责搜索和获取图片列表中的缩略图,ImageRetriever类负责用户从列表选择时下载原始版本的图片。本文用这个类来描述SwingWorker类的主要功能。图4显示程序的整个外观。
图4使用SwingWorkere类创建响应灵活程序界面
回顾Swing线程基础
一个Swing程序中一般有下面三种类型的线程:
* 初始化线程(Initial Thread)
* UI事件调度线程(EDT)
* 任务线程(Worker Thread)
每个程序必须有一个main方法,这是程序的入口。该方法运行在初始化或启动线程上。初始化线程读取程序参数并初始化一些对象。在许多Swing程序中,该线程主要目的是启动程序的图形用户界面(GUI)。一旦GUI启动后,对于大多数事件驱动的桌面程序来说,初始化线程的工作就结束了。
Swing程序只有一个用EDT,该线程负责GUI组件的绘制和更新,通过调用程序的事件处理器来响应用户交互。所有事件处理都是在EDT上进行的,程序同UI组件和其基本数据模型的交互只允许在EDT上进行,所有运行在EDT上的任务应该尽快完成,以便UI能及时响应用户输入。
Swing编程时应该注意以下两点:
1.从其他线程访问UI组件及其事件处理器会导致界面更新和绘制错误。
2.在EDT上执行耗时任务会使程序失去响应,这会使GUI事件阻塞在队列中得不到处理。
3.应使用独立的任务线程来执行耗时计算或输入输出密集型任务,比如同数据库通信、访问网站资源、读写大树据量的文件。
总之,任何干扰或延迟UI事件的处理只应该出现在独立任务线程中;在初始化线程或任务线程同Swing组件或其缺省数据模型进行的交互都是非线程安全性操作。
SwingWorker类帮你管理任务线程和Swing EDT之间的交互,尽管SwingWorker不能解决并发线程中遇到的所有问题,但的确有助于分离Swing EDT和任务线程,使它们各负其责:对于EDT来说,就是绘制和更新界面,并响应用户输入;对于任务线程来说,就是执行和界面无直接关系的耗时任务和I/O密集型操作。
使用合适线程
初始化线程运行程序的main方法,该方法能处理许多任务。但在典型的Swing程序中,其主要任务就是创建和运行应用程序的界面。创建UI的点,也就是程序开始将控制权转交给UI时的点,往往是同EDT交互出现问题的第一个地方。
Image Search示例的主类是MainFrame,从其main方法启动。许多程序使用下面方法启动界面,但这是错误的启动UI界面的方法:
public class MainFrame extends javax.swing.JFrame {
...
public static void main(String[] args) {
new MainFrame().setVisible(true);
}
}
尽管这种错误出现在开始,但仍然违反了不应在EDT外的其他线程同Swing组件交互的原则。这个错误尤其容易犯,线程同步问题虽然不是马上显示出来,但是还要注意避免这样书写。
正确启动UI界面应该如下:
public class MainFrame extends javax.swing.JFrame {
...
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new MainFrame().setVisible(true);
}
});
}
}
使用NetBeans IDE的开发者应该对这段代码很熟悉,NetBeans通常会自动生成这段代码。这段启动代码虽然和SwingWorker没有直接关系,但是这个编程范式很重要。SwingUtilities类包含一些静态方法帮你同UI组件交互,其中invokeLater方法意思是在EDT上执行其Runnable任务。Runnable接口定义了可作为独立线程执行的任务。
在初始化线程中使用invokeLater方法能正确的初始化程序界面。就像前面文章所提到的,此方法是异步执行的,也就是说调用会立即返回。创建界面后,大部分初始化线程基本上就结束了。
通常有两种办法调用此方法:
* SwingUtilities.invokeLater
* EventQueue.invokeLater
两个方法都是正确的,选择任何一个都可以。实际上,SwingUtilities版只是一个薄薄的封装方法,它直接转而调用EventQueue.invokeLater。因为Swing框架本身经常调用SwingUtilities,使用SwingUtilities可以减少程序引入的类。
另种将任务放到EDT执行的方法是SwingUtilities.invokeAndWait,不像invokeLater,invokeAndWait方法是阻塞执行的,它在EDT上执行Runnnable任务,直到任务执行完了,该方法才返回调用线程。
invokeLater和invokeAndWait都在事件派发队列中的所有事件都处理完之后才执行它们的Runnable任务,也就是说,这两个方法将Runnable任务放在事件队列的末尾。
注意:虽然可以在其他线程上调用invokeLater,也可以在EDT上调用invokeLater,但是千万不要在EDT线程上调用invokeAndWait方法!很容易理解,这样做会造成线程竞争,程序就会陷入死锁。
将EDT线程仅用于GUI任务
Swing框架负责管理组件绘制、更新以及EDT上的线程处理。可以想象,该线程的事件队列很繁忙,几乎每一次GUI交互和事件都是通过它完成。事件队列的上任务必须非常快,否则就会阻塞其他任务的执行,使队列里阻塞了很多等待执行的事件,造成界面响应不灵活,让用户感觉到界面响应速度很慢,使他们失去兴趣。理想情况下,任何需时超过30到100毫秒的任务不应放在EDT上执行,否则用户就会觉察到输入和界面响应之间的延迟。
幸运的是,不会仅仅因为有复杂的任务、计算或输入输出密集任务需要作为GUI事件处理任务执行,Swing的性能就要有所降低。毕竟有许多桌面程序执行耗时任务,比如处理电子表格公式、跨越网络查询数据库、通过Internet向其他程序发送信息。即使有这些任务,界面仍然可以让用户感觉到响应灵活、快捷。编写响应灵活的程序需要创建和管理独立于EDT的线程。
在Image Search程序中,有两个事件如果完全在EDT上处理就会降低界面的响应速度:图像搜索处理和选中图片下载处理。
两个事件处理都要访问Web服务,这些服务通常要许多秒后才能响应,在此期间,如果程序在EDT上进行Web服务交互,用户就不能取消搜索或者同界面交互,像这两种都不应该在EDT上运行。
图5显示了在A和B点之间,EDT不能处理UI事件,AB两点之间代表了程序访问Flickr网站Web服务的IO操作时间:
图5. 在执行Web服务期间EDT不能响应UI事件
javax.swing.SwingWorker类是Java SE 6中新出现的类,使用SwingWorker,程序能启动一个任务线程来异步查询,并马上返回EDT线程。图6显示了使用SwingWorker后,事件处理立即返回,允许EDT继续执行后续的UI事件。
图6.使用任务线程,程序能够在避免在EDT上执行I/O密集型任务
SwingWorker基础
本节简要介绍SwingWorker的功能。SwingWorker的定义如下:
public abstract class SwingWorker<T,V> extends Object implements RunnableFuture
SwingWorker是抽象类,因此必须继承它才能执行所需的特定任务。注意该类有两个类型参数:T及V。T是doInBackground和get方法的返回类型,V是publish和process方法要处理的数据类型。后文将作详细解释。 该类实现了java.util.concurrent.RunnableFuture接口。RunnableFuture接口是Runnable和Future两个接口的简单封装。由于SwingWorker实现了Runnable接口,因此SwingWorker有一个run方法。Runnable对象一般作为线程的一部分执行,当Thread对象启动时,它激活Runnable对象的run方法。由于SwingWorker实现了Future接口,因此SwingWorker产生类型为T的结果值并提供同线程交互的方法。SwingWorker实现以下接口方法:
* boolean cancel(boolean mayInterruptIfRunning)
* T get()
* T get(long timeout, TimeUnit unit)
* boolean isCancelled()
* boolean isDone()
SwingWorker实现了所有的接口方法,实际上你仅需要实现以下SwingWorker的抽象方法:
protected T doInBackground() throws Exception
doInBackground方法作为任务线程的一部分执行,它负责完成线程的基本任务,并以返回值来作为线程的执行结果。继承类须覆盖该方法并确保包含或代理任务线程的基本任务。不要直接调用该方法,应使用任务对象的execute方法来调度执行。
在获得执行结果后应使用SwingWorker的get方法获取doInBackground方法的结果。可以在EDT上调用get方法,但该方法将一直处于阻塞状态,直到任务线程完成。最好只有在知道结果时才调用get方法,这样用户便不用等待。为防止阻塞,可以使用isDone方法来检验doInBackground是否完成。另外调用方法get(long timeout, TimeUnit unit)将会一直阻塞直到任务线程结束或超时。获取任务结果的最好地方是在done方法内:
protected void done()
在doInBackground方法完成之后,SwingWorker调用done方法。如果任务需要在完成后使用线程结果更新GUI组件或者做些清理工作,可覆盖done方法来完成它们。这儿是调用get方法的最好地方,因为此时已知道线程任务完成了,SwingWorker在EDT上激活done方法,因此可以在此方法内安全地和任何GUI组件交互。
没必要等到线程完成就可以获得中间结果。中间结果是任务线程在产生最后结果之前就能产生的数据。当任务线程执行时,它可以发布类型为V的中间结果,覆盖process方法来处理中间结果。后文还将提供这些方法的更多详细信息。当属性改变时,SwingWorker实例能通知处理器,SwingWorker有两个重要的属性:状态和进程。任务线程有几种状态,以下面SwingWorker.StateValue枚举值来表示:
* PENDING
* STARTED
* DONE
任务线程一创建就处于PENDING状态,当doInBackground方法开始时,任务线程就进入STARTED状态,当doInBackground方法完成后,任务线程就处于DONE状态,随着线程进入各个阶段,SwingWorker超类自动设置这些状态值。你可以添加处理器,当这些属性发生变化来接收通知。
最后,任务对象有一个进度属性,随着任务进展时,可以将这个属性从0更新到100标识任务进度,当该属性发生变化时,任务通知处理器进行处理。
实现简单的ImageRetriever
当点击列表所略图时,事件处理器创建了一个ImageRetriever实例并执行之。ImageRetriever下载选中的图片并在列表下面展示它。当实现SwingWorker子类,须指定doInBackground和get方法返回值的类型。因为ImageRetriever并不生成中间结果,它使用特殊类型Void作为中间类型,ImageRetriever的任务的结果是一图片,因此使用Icon类型作为doInBackground和get方法的返回类型,下面代码显示了ImageRetriever的大部分实现:
public class ImageRetriever extends SwingWorker<Icon, Void> {
private ImageRetriever() {}
public ImageRetriever(JLabel lblImage, String strImageUrl) {
this.strImageUrl = strImageUrl;
this.lblImage = lblImage;
}
@Override
protected Icon doInBackground() throws Exception {
Icon icon = retrieveImage(strImageUrl);
return icon;
}
private Icon retrieveImage(String strImageUrl)
throws MalformedURLException, IOException {
InputStream is = null;
URL imgUrl = null;
imgUrl = new URL(strImageUrl);
is = imgUrl.openStream();
ImageInputStream iis = ImageIO.createImageInputStream(is);
Iterator<ImageReader> it =
ImageIO.getImageReadersBySuffix("jpg");
ImageReader reader = it.next();
reader.setInput(iis); ...
Image image = reader.read(0);
Icon icon = new ImageIcon(image);
return icon;
}
@Override
protected void done() {
Icon icon = null;
String text = null;
try {
icon = get();
} catch (Exception ignore) {
ignore.printStackTrace();