Swing应用程序员常见的错误是误用Swing事件调度线程(Event DispatchThread,EDT)。他们要么从非UI线程访问UI组件;要么不考虑事件执行顺序;要么不使用独立任务线程而在EDT线程上执行耗时任务,结果使编写的应用程序变得响应迟钝、速度很慢。耗时计算和输入/输出(IO)密集型任务不应放在SwingEDT上运行。发现这种问题的代码并不容易,但Java SE6提供了javax.swing.SwingWorker类,使修正这种代码变得更容易。
使用SwingWorker,程序能启动一个任务线程来异步查询,并马上返回EDT线程,允许EDT继续执行后续的UI事件。
SwingWorker类帮你管理任务线程和EDT之间的交互,尽管SwingWorker不能解决并发线程中遇到的所有问题,但的确有助于分离SwingEDT和任务线程,使它们各负其责:对于EDT来说,就是绘制和更新界面,并响应用户输入;对于任务线程来说,就是执行和界面无直接关系的耗时任务和I/O密集型操作。
SwingWorker结构
SwingWoker实现了java.util.concurrent.RunnableFuture接口。RunnableFuture接口是Runnable和Future两个接口的简单封装。
因为实现了Runnable,所以有run方法,调用FutureTask.run()
因为实现了Future,所以有:
public abstract class SwingWorker<T, V> implements RunnableFuture<T> { //FutureTask private final FutureTask<T> future; public final void run() { future.run(); } public SwingWorker() { Callable<T> callable = new Callable<T>() { //1. 任务线程一创建就处于PENDING状态 public T call() throws Exception { //2. 当doInBackground方法开始时,任务线程就进入STARTED状态 setState(StateValue.STARTED); return doInBackground(); } }; //FutureTask future = new FutureTask<T>(callable) { @Override protected void done() { doneEDT(); //3. 当doInBackground方法完成后,任务线程就处于DONE状态 setState(StateValue.DONE); } }; state = StateValue.PENDING; propertyChangeSupport = new SwingWorkerPropertyChangeSupport(this); doProcess = null; doNotifyProgressChange = null; } }
SwingWorker有两个类型参数:T及V。T是doInBackground和get方法的返回类型;V是publish和process方法要处理的数据类型
SwingWorker实例不可复用,每次执行任务必须生成新的实例。
doInBackground和get和done
//1、doInBackground protected abstract T doInBackground() throws Exception ; //2、get --不可重写 public final T get() throws InterruptedException, ExecutionException { return future.get(); } //3、done protected void done() { } /** * Invokes {@code done} on the EDT. */ private void doneEDT() { Runnable doDone = new Runnable() { public void run() { done(); } }; //SwingWorker在EDT上激活done() if (SwingUtilities.isEventDispatchThread()) { doDone.run(); } else { doSubmit.add(doDone); } }
doInBackground方法作为任务线程的一部分执行,它负责完成线程的基本任务,并以返回值来作为线程的执行结果。继承类须覆盖该方法并确保包含或代理任务线程的基本任务。不要直接调用该方法,应使用任务对象的execute方法来调度执行。
在获得执行结果后应使用SwingWorker的get方法获取doInBackground方法的结果。可以在EDT上调用get方法,但该方法将一直处于阻塞状态,直到任务线程完成。最好只有在知道结果时才调用get方法,这样用户便不用等待。为防止阻塞,可以使用isDone方法来检验doInBackground是否完成。另外调用方法get(longtimeout, TimeUnitunit)将会一直阻塞直到任务线程结束或超时。get获取任务结果的最好地方是在done方法内。
在doInBackground方法完成之后,SwingWorker调用done方法。如果任务需要在完成后使用线程结果更新GUI组件或者做些清理工作,可覆盖done方法来完成它们。这儿是调用get方法的最好地方,因为此时已知道线程任务完成了,SwingWorker在EDT上激活done方法,因此可以在此方法内安全地和任何GUI组件交互。
SwingWorker testWorker = new SwingWorker<Icon , Void>(){ @Override protected Icon doInBackground() throws Exception { Icon icon = retrieveImage(strImageUrl); return icon; } protected void done(){ //没有必要用invokeLater!因为done()本身是在EDT中执行的 SwingUtilities.invokeLater(new Runnable(){ @Override public void run() { Icon icon= get(); lblImage.setIcon(icon); //lblImage可通过构造函数传入 } } //execute方法是异步执行,它立即返回到调用者。在execute方法执行后,EDT立即继续执行 testWorker.execute();
- 指定Icon作为doInBackground和get方法的返回类型
- 因为并不产生任何中间数据,所以指定Void类型作为中间结果类型。
publish和process
SwingWorker在doInBackground方法结束后才产生最后结果,但任务线程也可以产生和公布中间数据。有时没必要等到线程完成就可以获得中间结果。
中间结果是任务线程在产生最后结果之前就能产生的数据。当任务线程执行时,它可以发布类型为V的中间结果,通过覆盖process方法来处理中间结果。
任务对象的父类会在EDT线程上激活process方法,因此在process方法中程序可以安全的更新UI组件。
//SwingWorker.publish protected final void publish(V... chunks) { synchronized (this) { if (doProcess == null) { doProcess = new AccumulativeRunnable<V>() { @Override public void run(List<V> args) { //调用process process(args); } @Override protected void submit() { doSubmit.add(this); } }; } } doProcess.add(chunks); } //SwingWorker.process 在EDT中调用 protected void process(List<V> chunks) { }
当从任务线程调用publish方法时,SwingWorker类调度process方法。有意思的是process方法是在EDT上面执行,这意味着可以同Swing组件和其模型直接交互。
例如可让publish处理Icon类型的数据;则doInBackground对应应该返回List<Icon>类型
使用publish方法来发布要处理的中间数据,当ImageSearcher线程下载缩略图时,它会随着下载而更新图片信息列表,还会发布每一批图像信息,以便UI能在图片数据到达时显示这些图片。
如果SwingWorker通过publish发布了一些数据,那么也应该实现process方法来处理这些中间结果,任务对象的父类会在EDT线程上激活process方法,因此在此方法中程序可以安全的更新UI组件。
private void retrieveAndProcessThumbnails(List<ImageInfo> infoList) { for (int x=0; x <infoList.size() && !isCancelled(); ++x) { ImageInfo info = infoList.get(x); String strImageUrl = String.format("%s/%s/%s_%s_s.jpg", IMAGE_URL, info.getServer(), info.getId(), info.getSecret()); Icon thumbNail = retrieveThumbNail(strImageUrl); info.setThumbnail(thumbNail); //发布中间结果 publish(info); setProgress(100 * (x+1)/infoList.size()); } } /** * Process is called as a result of this worker thread's calling the * publish method. This method runs on the event dispatch thread. * * As image thumbnails are retrieved, the worker adds them to the * list model. * */ @Override protected void process(List<ImageInfo> infoList) { for(ImageInfo info: infoList) { if (isCancelled()) { //见下节 break; } //处理中间结果 model.addElement(info); } }
cancel和isCancelled
public final boolean cancel(boolean mayInterruptIfRunning) { return future.cancel(mayInterruptIfRunning); } /** * {@inheritDoc} */ public final boolean isCancelled() { return future.isCancelled(); }
如果想允许程序用户取消任务,实现代码要在SwingWorker子类中周期性地检查取消请求。调用isCancelled方法来检查是否有取消请求。检查的时机主要是:
- doInBackground方法的子任务在获取每个缩略图之前
- process方法中在更新GUI列表模型之前
- done方法中在更新GUI列表模型最终结果之前
【例】判断是否被取消(见上例)
【例】取消
可以通过调用其cancel方法取消SwingWorker线程
private void searchImages(String strSearchText, int page) { if (searcher != null && !searcher.isDone()) { // Cancel current search to begin a new one. // You want only one image search at a time. //检查现有线程是否正在运行,如果正在运行则调用cancel来取消 searcher.cancel(true); searcher = null; } ... // Provide the list model so that the ImageSearcher can publish // images to the list immediately as they are available. searcher = new ImageSearcher(listModel, API_KEY, strEncodedText, page); searcher.addPropertyChangeListener(listenerMatchedImages); progressMatchedImages.setIndeterminate(true); // Start the search! searcher.execute(); // This event thread continues immediately here without blocking. }
setProgress和getProgress
任务对象有一个进度属性,随着任务进展时,可以将这个属性从0更新到100标识任务进度。当你在任务实例内处理这些信息时,你可以调用setProgress方法来更新这个属性。
当该属性发生变化时,任务通知处理器进行处理。(?)
//javax.imageio.ImageReader reader reader.addIIOReadProgressListener(new IIOReadProgressListener() { ... public void imageProgress(ImageReader source, float percentageDone) { setProgress((int) percentageDone); } public void imageComplete(ImageReader source) { setProgress(100); } });
客户端调用1、更新进度条事件处理
/** * ProgressListener listens to "progress" property changes in the SwingWorkers that search and load images. */ class ProgressListener implements PropertyChangeListener { // Prevent creation without providing a progress bar. private ProgressListener() {} ProgressListener(JProgressBar progressBar) { this.progressBar = progressBar; this.progressBar.setValue(0); } public void propertyChange(PropertyChangeEvent evt) { String strPropertyName = evt.getPropertyName(); if ("progress".equals(strPropertyName)) { progressBar.setIndeterminate(false); int progress = (Integer)evt.getNewValue(); progressBar.setValue(progress); } } private JProgressBar progressBar; }
客户端调用2、添加监听
private void listImagesValueChanged(ListSelectionEvent evt) { ... ImageInfo info = (ImageInfo) listImages.getSelectedValue(); String id = info.getId(); String server = info.getServer(); String secret = info.getSecret(); // No need to search an invalid thumbnail image if (id == null || server == null || secret == null) { return; } String strImageUrl = String.format(IMAGE_URL_FORMAT, server, id, secret); retrieveImage(strImageUrl); ... } private void retrieveImage(String imageUrl) { // SwingWorker,不可复用 ImageRetriever imgRetriever = new ImageRetriever(lblImage, imageUrl); progressSelectedImage.setValue(0); // Listen for changes in the "progress" property. // You can reuse the listener even though the worker thread will be a new SwingWorker. imgRetriever.addPropertyChangeListener(listenerSelectedImage); progressSelectedImage.setIndeterminate(true); // Tell the worker thread to begin with this asynchronous method. imgRetriever.execute(); // This event thread continues immediately here without blocking. }