如果所有任务的执行时间都较短(并且应用程序中不包含执行时间较长的非GUI部分),那么整个应用程序都可以在事件线程内部运行,并且完全不用关心线程。然而,在复杂的GUI 应用程序中可能包含一些执行时间较长的任务,并且可能超过了用户可以等待的时间,例如拼写检查、后台编辑或者获取远程资源等。这些任务必须在另一个线程中运行,才能使得GUI在运行时保持高响应性。
Swing 使得在事件线程中运行任务更容易,但(在Java 6之前)并没有提供任何机制来帮助GUI任务执行其他线程中的代码。然而在这里不需要借助于Swing:可以创建自己的Executor 来执行长时间的任务。对于长时间的任务,可以使用缓存线程池。只有GUI应用程序很少会发起大量的长时间任务,因此即使线程池可以无限制地增长也不会有太大的风险。
首先来看一个简单的任务,该任务不支持取消操作和进度指示,也不会在完成后更新GUI,我们之后再将这些功能依次添加进来。在程序清单9-4中给出了一个与某个可视化组件绑定的监听器,它将一个长时间的任务提交给一个Executor。尽管有两个层次的内部类,但通过这种方式使某个GUI任务启动另一个任务还是很简单的:在事件线程中调用UI动作监听器,然后将一个Runnable提交到线程池中执行。
这个示例通过“Fire and Forget”与方式将长时间任务从事件线程中分离出来,这种方式可能并不是非常有用。在执行完一个长时间的任务后,通常会产生某种可视化的反馈。但你并不能从后台线程中访问这些表现对象,因此任务在完成时必须向事件线程提交另一个任务来更新用户界面。
程序清单9-4 将一个长时间任务绑定到一个可视化组件
ExecutorService backgroundExec =Executors. newCachedThreadPool();
…
button,addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
backgroundExec. execute(new Runnable(){
public void run(){doBigComputation();}
} ) ;
} } ) ;
程序清单9-5给出了如何实现这个功能的方式,但此时已经开始变得复杂了,即已经有了三层的内部类。动作监听器首先使按钮无效,并设置一个标签表示正在进行某个计算,然后将一个任务提交给后台的Executor。当任务完成时,它会在事件线程中增加另一个任务,该任务将重新激活按钮并恢复标签文本。
程序清单9-5支持用户反馈的长时间任务
button,addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
button. setEnabled(false);
label. setText("busy");
backgroundExec. execute(new Runnable(){
public void run(){
try {
doBigComputation();
}finally {
GuiExecutor. instance(). execute(new Runnable(){
public void run(){
button. setEnabled(true);
label. setText("idle");
}
} ) ;
}
}
} ) ;
﹞ |
} ) ;
在按下按钮时触发的任务中包含3个连续的子任务,它们将在事件线程与后台线程之间交替运行。第一个子任务更新用户界面,表示一个长时间的操作已经开始,然后在后台线程中启动第二个子任务。当第二个子任务完成时,它把第三个子任务再次提交到事件线程中运行,第三个子任务也会更新用户界面来表示操作已经完成。在GUI应用程序中,这种“线程接力”是处理长时间任务的典型方法。
取消
当某个任务在线程中运行了过长时间还没有结束时,用户可能希望取消它。你可以直接通过线程中断来实现取消操作,但是一种更简单的办法是使用Future,专门用来管理可取消的任务。
如果调用Future的cancel方法,并将参数mayInterruptIfRunning设置为true,那么这个Future 可以中断正在执行任务的线程。如果你编写的任务能够响应中断,那么当它被取消时就可以提前返回。在程序清单9-6给出的任务中,将轮询线程的中断状态,并且在发现中断时提前返回。
程序清单9-6 取消一个长时间任务
Future> runningTask=null;//线程封闭
startButton. addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
if (runningTask !=nul1){
runningTask =backgroundExec. submit(new Runnable(){
public void run(){
while (moreWork()){
if (Thread. currentThread(). isInterrupted()){
cleanUpPartialWork();
break;
}
doSomeWork();
}
}
} ) ;
} ;
} } ) ;
cancelButton. addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent event){
if (runningTask !=null)
runningTask. cancel(true);
} } ) ;
由于runningTask被封闭在事件线程中,因此在对它进行设置或检查时不需要同步,并且“开始”按钮的监听器可以确保每次只有一个后台任务在运行。然而,当任务完成时最好能通知按钮监听器,例如说可以禁用“取消”按钮。我们将在下一节解决这个问题。
进度标识和完成标识
通过Future 来表示一个长时间的任务,可以极大地简化取消操作的实现。在FutureTask中也有一个done 方法同样有助于实现完成通知。当后台的Callable 完成后,将调用done。通过done 方法在事件线程中触发一个完成任务,我们能够构造一个BackgroundTask类,这个类将提供一个在事件线程中调用的onCompletion方法,如程序清单9-7所示。
程序清单9-7支持取消,完成通知以及进度通知的后台任务类
abstract class BackgroundTask
private final FutureTask
private class Computation extends FutureTask
public Computation(){
super(new Callable
public v call() throws Exception {
return BackgroundTask. this. compute();
}
} ) ;
}
protected final void done(){
GuiExecutor. instance(). execute(new Runnable(){
public void run(){
V value =null;
Throwable thrown =null;
boolean cancelled =false;
try {
value =get();
}catch (ExecutionException e){
thrown =e. getCause();
}catch (CancellationException e){
cancelled =true;
}catch (InterruptedException consumed){
}finally {
onCompletion(value, thrown, cancelled);
}
} ;
} ) ;
}
}
protected void setProgress(final int current, final int max){
GuiExecutor. instance(). execute(new Runnable(){
public void run(){onProgress(current, max);}
} ) ;
}
// 在后台线程中被取消
protected abstract v compute() throws Exception;
// 在事件线程中被取消
protected void onCompletion(V result, Throwable exception,
boolean cancelled){}
protected void onProgress(int current, int max){}
// Future的其他方法
}
BackgroundTask还支持进度标识。compute 方法可以调用setProgress方法以数字形式来指示进度。因而在事件线程中调用onProgress,从而更新用户界面以显示可视化的进度信息。
要想实现BackgroundTask,你只需要实现compute,该方法将在后台线程中调用。也可以改写onCompletion和onProgress,这两个方法也会在事件线程中调用。
基于FutureTask构造的BackgroundTask 还能简化取消操作。Compute不会检查线程的中断状态,而是调用Future. isCancelled。程序清单9-8 通过BackgroundTask重新实现了程序清单9-6中的示例程序。
程序清单9-8 通过BackgroundTask来执行长时间的并且可取消的任务
startButton. addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
class CancelListener implements ActionListener {
BackgroundTask>task;
public void actionPerformed(ActionEvent event){
if (task !=null)
task. cancel(true);
}
}
final CancelListen èr listener =new CancelListener();
listener. task =new BackgroundTask
public Void compute(){
while (moreWork()&&lisCancelled())
doSomeWork();
return null;
}
public void onCompletion(boolean cancelled, string s,
Throwable exception){
cancelButton. removeActionListener(listener);
label. setText("done");
}
} ;
cancelButton. addActionListener(listener);
backgroundExec. execute(listener. task);
}
} ) ;
SwingWorker
我们已经通过FutureTask和Executor 构建了一个简单的框架,它会在后台线程中执行长时间的任务,因此不会影响GUI的响应性。在任何单线程的GUI框架都可以使用这些技术,而不仅限于Swing。在Swing中,这里给出的许多特性是由SwingWorker类提供的,包括取消、完成通知、进度指示等。在《The Swing Connection》和《The Java Tutorial》等资料中介绍了不同版本的SwingWorker,并在Java 6中包含了一个更新后的版本。