长时间的GUI任务

如果所有任务的执行时间都较短(并且应用程序中不包含执行时间较长的非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 BackgroundTaskimplements Runnable, Future{

private final FutureTaskcomputation =new Computation();

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 {

BackgroundTasktask;

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中包含了一个更新后的版本。

你可能感兴趣的:(java,ui,jvm)