
4.6 在GUI中用SwingWorker实现异步交互
对于4.4.1节创建的EchoClient类,也可以改成用图形用户界面(Graphical User Interface,GUI)来接受用户输入的字符串。例程4-8的gui.EchoClient类提供了图形用户界面,用户在文本框JTextField中输入字符串,gui.EchoClient类会在一个JTextPane文本面板中显示服务器端返回的响应结果。
例程4-8 gui.EchoClient类



以上程序创建的用户界面如图4-1所示。当用户在JTextField文本框中输入一些字符,然后回车,就会触发ActionEvent事件。EchoClient对该事件的处理过程如下。
(1)获取用户在文本框输入的消息。
(2)向服务器端发送消息。
(3)接收服务器端返回的响应结果。
(4)在文本面板上显示响应结果。
当EchoClient类在处理ActionEvent事件时,如果在接收服务器端的返回响应结果时,长时间进入阻塞状态,那么会出现什么情况呢?下面通过实验来演示实际产生的运行效果。步骤如下。
(1)修改4.3.1节的EchoServer,使它在向客户端发送响应结果之前,故意睡眠若干秒,使得客户端在接收响应结果时进入阻塞状态。修改后的服务类为gui.EchoServer。主要的修改代码如下。


(2)先运行gui.EchoServer类,再运行gui.EchoClient类。在EchoClient类的用户界面的文本框中输入一些字符再按Enter键,不断快速重复这一操作。会发现用户界面常常被“卡”住,即来不及响应用户的操作。
为什么用户界面会被“卡”住呢?下面来分析原因。当gui.EchoClient类运行时,共有两个线程在工作。
·main主线程:负责执行main()方法,把创建EchoClient对象的任务提交给EDT线程。
·EDT线程(Event Dispatch Thread,事件分派线程):这是Java虚拟机为图形用户界面自动提供的线程。该线程的工作包括:(1)绘制和刷新图形用户界面;(2)监听和处理由图形用户界面触发的各种事件;(3)执行其他线程提交的任务,例如在本例中,main主线程向EDT线程提交了创建EchoClient对象的任务。
当gui.EchoClient类开始运行时,main主线程执行完main()方法后就结束生命周期,接下来只有EDT线程以单线程的方式运行。它要全盘兼顾对图形用户界面的绘制和刷新,监听和处理由图形用户界面触发的各种事件,势必应接不暇、顾此失彼。当EDT线程在处理本范例中的ActionEvent事件时,它因为等待服务器端返回响应结果而进入阻塞状态,所以没办法立即响应用户在图形用户界面上继续输入字符并回车的操作,这样就产生界面很“卡”的情况。
如何解决这一问题呢?这就需要想办法由其他线程来分担EDT线程的一些工作,让EDT线程主要负责对图形用户界面的绘制和刷新,监听由图形用户界面触发的各种事件,而在处理事件时,并不用亲力亲为,只要把具体的事件处理任务交给其他线程去执行就行了。EDT线程转交完事件处理任务后,就能立刻继续负责对用户界面的维护和事件监听。
接下来的一个问题是,EDT线程把具体的事件处理任务交给哪个线程呢?一种办法是交给开发人员自定义的线程。但是,图形用户界面中的组件都是非线程安全的。所谓非线程安全,指假如有几个线程(包括EDT线程和开发人员自定义的线程)并发对界面中的组件进行外观或组件中文本的修改,可能会导致界面无法正常显示。所以,还有一种更可靠安全的办法是采用Java Swing API提供的SwingWorker类,它和EDT线程都由JDK本身提供,它们内部的配合更加默契。
4.6.1 SwingWorker类的用法
SwingWorker类的定义如下。

SwingWorker类实现了RunnableFuture 接口,而RunnableFuture 接口又继承了Future接口和Runnable接口。所以SwingWorker类支持异步运算。SwingWorker<T,V>有两个参数。
·“T”表示异步运算的最终结果。SwingWorker类的doInBackground()和 get()方法返回最终结果。
·“V”表示异步运算的中间运算数据。SwingWorker类的 publish()方法产生中间运算数据,process方法()处理中间运算数据。
SwingWorker类主要包含以下方法。
·protected abstract T doInBackground()
该方法中包含了主要的后台处理任务,由SwingWorker工作线程来执行。执行完毕,会返回最终运算结果。如果执行中遇到异常,则可以抛出该异常。
·protected void publish(V chunks)
参数chunks表示运算中的中间运算数据,该方法通常由SwingWorker工作线程来执行。在doInBackground()方法中可以调用此方法来发送一些中间运算数据。publish()方法会通知EDT线程执行process()方法。
·protected void process(List<V> chunks)
该方法由EDT线程执行。当SwingWorker工作线程执行一次publish()方法后,就会导致EDT线程执行一次process()方法,process()方法会处理由publish()方法发送的中间运算数据。process()方法的chunks参数就表示中间运算数据。
·protected void done()
该方法由EDT线程执行。当SwingWorker工作线程执行完doInBackground()方法后,EDT线程会自动执行done()方法。由此可见,SwingWorker类与EDT线程内部配合非常默契。这种配合默契程度是开发人员自定义的线程很难达到的。
·public void execute()
当其他线程(例如main主线程或EDT线程)调用了一个SwingWorker对象的execute()方法后,该方法就会把SwingWorker任务提交给SwingWorker工作线程池。SwingWorker工作线程池会委派一个处于空闲状态的SwingWorker工作线程来执行SwingWorker任务,确切地说,就是执行SwingWorker对象的doInBackground()方法。
·public T get()
由其他线程来调用,获得SwingWorker任务的最终运算结果。get()方法会等待doInBackground()方法计算完成,返回doInBackground()方法产生的最终运算结果。
·public boolean isDone()
由其他线程来调用,判断是否完成了整个SwingWorker任务,如果任务完成,就返回true。
·public boolean cancel(boolean mayInterruptIfRunning)
由其他线程来调用,取消SwingWorker任务。如果取消成功,就返回 true。假如任务还没开始执行,那么cancel()方法使得该任务永远不会执行。如果任务正在执行中,并且参数mayInterruptIfRunning为true,就会取消这一执行中的任务。
4.6.2 用SwingWorker类来展示进度条
在Swing API中,JProgressBar表示进度条,它能直观地告诉用户某个任务执行的进度。JProgressBar的setValue(int value)方法依据参数value来显示进度条的长度。当程序不断调用setValue(int value)方法,并且每次提供不同的value参数值时,就会使得进度条不断动态更新。
在以下例程4-9的gui.BarDemo类中,利用SwingWorker与EDT线程的默契合作,就能不断更新进度条,展示执行一个写文件任务的进度。
例程4-9 BarDemo.java


BarDemo类创建的图形用户界面如图4-2所示。
当用户在界面上按下“Begin”按钮后,所触发的ActionEvent事件由BarDemo构造方法中定义的匿名ActionListener来处理。

以上actionPerformed()方法由EDT线程来执行。该方法创建一个ProgressBarHandler对象,再调用它的execute()方法。execute()方法向SwingWorker工作线程池提交了一个ProgressBarHandler任务。SwingWorker工作线程池会委派特定的SwingWorker工作线程来执行ProgressBarHandler对象的doInBackground()方法。
ProgressBarHandler类是BarDemo类的内部类。ProgressBarHandler类继承了SwingWorker类。ProgressBarHandler类在doInBackground()方法中执行一个耗时的操作,向文件中不断写入数据,并且定期通过publish()方法发送中间运算数据。
ProgressBarHandler类的process()方法由EDT线程执行,根据当前的中间运算数据来绘制进度条。
当SwingWorker工作线程执行完doInBackground()方法,EDT线程会调用done()方法,该方法向用户显示一个“任务完成”的消息框。
提示
提示:如果用户不断在BarDemo的用户界面上按下“Begin”按钮,就会导致若干SwingWorker工作线程并发执行ProgressBarHandler任务,并发修改进度条,这会导致进度条无法正常显示。解决这一问题的办法是当用户按下“Begin”按钮后,就调用按钮的setEnabled(false)方法,使得该按钮暂时失效,直到ProgressBarHandler任务完成,才使按钮重新有效。并且提供一个“Cancel”按钮方法,允许用户中途取消ProgressBarHandler任务。在程序中,调用SwingWorker类的cancel()方法可以取消任务。
4.6.3 用SwingWorker类实现异步的AsynEchoClient
例程4-10的gui.AsynEchoClient类创建的用户界面和4.6节的例程4-1的EchoClient类相同。区别在于,在AsynEchoClient类中,定义了继承SwingWorker类的ActionEventHandler类,它在doInBackGround()方法中负责接收服务器端的响应结果。
例程4-10 AsynEchoClient.java


当SwingWorker工作线程执行完ActionEventHandler对象的doInBackground()方法,EDT线程就会执行ActionEventHandler对象的done()方法,该方法会在文本面板中显示服务器端发送的响应结果。
先运行gui.EchoServer类,再运行gui.AsynEchoClient类,当用户在AsynEchoClient类的用户界面的文本框中输入一些字符再回车,接着不断快速重复这一操作后,不会出现界面很“卡”的情况。这是因为每次处理ActionEvent事件的具体任务都是由SwingWorker工作线程池中的工作线程去执行的,所以EDT线程有更多的时间来响应用户与图形用户界面之间的交互操作。