UML软件工程组织

Swing线程的最后讨论 -- 利用异步模型
FooSleeper 翻译

原文来自java.sun.com

作者:Joseph Bowbeer

本文并不属于任何系列,但它是The Swing Connection中发表的第三篇关于在Swing中使用线程的文章。
第一篇文章《线程与Swing》,解释了Swing的单线程规则。这篇文章现在可以在The Swing Connection Archive找到。
第二篇文章《使用Swing Worker线程》,演示了如何使用SwingWorker线程工具类。它也可以在存档中找到。
本文介绍了修订过的SwingWorker类,并演示了和基于模型的组件(model-based components)如JTable和JTree同时使用线程。
要理解本文呈现的材料,熟悉SwingWorker和JTable和JTree组件会有所帮助。你可以在存档索引中找到关于JTable和JTree的文章。
本文分为下面几个主要部分:
介绍和回顾
动态树
远程树
SwingWorker修订版
下载
结论
参考资料
关于作者
---------------------------------------------------------------------------

介绍与回顾


在钻研树、表和异步模型之前,我首先回顾一下Swing的单线程规则(single-thread rule)并检验它的含义。
Swing的单线程规则是说,Swing组件在某一时刻仅能被一个线程访问。这个规则对gets和sets都有效,并且“一个线程”通常指的都是事件派发线程。
单线程规则应用于UI组件是非常相称的,因为UI组件往往都以单线程方式使用,并由用户发起大多数动作。构建线程安全的组件是困难而冗长乏味的:如果能避免则最好不过。但除了它的好处,单线程规则有更多的含义。
Swing组件所有的事件都在事件派发线程(event-dispatch thread)中发送和接收,除此之外才不遵守单线程规则。例如,属性变更事件(property-change events)应该在事件派发线程中发送,模型变更事件(model-change events)应该在事件派发线程中接收。
对于基于模型的组件如JTable和JTree来说,单线程规则意味着仅能在事件派发线程中访问模型本身。出于这个理由,模型中的方法必须执行得很快并且决不能阻塞,否则整个用户界面会响应迟钝。
但想象一下你有一个带TreeModel的JTree要访问一个远程服务器?你会发现当服务器不可用或过于繁忙时,你对的TreeModel的调用会阻塞,并使得整个用户界面被冻结。你能做些什么来改进界面的响应能力?
再假设你有一个带TableModel的JTable管理着网络上的一个设备?你会发现当被管理的设备繁忙或网络拥挤时,界面变得迟钝。你该怎么办?
这些困难的情况在召唤线程。当需求浮现,我们还是有几种途径在Swing中使用线程,尽管存在单线程规则。
本文展现了两种方法来使用线程访问缓慢的、远程的或其他异步的模型,并显示了如何在JTree和JTable组件中使用。(不仅仅由事件派发线程访问的模型被称为“异步”模型。)

-----------------------------------------------------------------------------

动态树


假设你有一个带TreeModel的JTree要访问远程服务器,但服务器又是缓慢或不可靠的,你该怎么办?
DynamicTree演示了如何使用后台线程动态展开JTree的节点。
图1是正在处理节点展开的DynamicTree。

图1
分裂模型(Split-model)设计

DynamicTree基于一种分裂模型设计(split-model design)。在这个设计中,真正的模型是一个异步模型,它可能是缓慢或不可靠的。JTree模型使用一个标准的同步模型来维持一个真正的模型的snapshot。(异步模型可能位于远程服务器,出于这个原因我通常称之为远程模型,并且我把Swing组件的模型称为本地模型。)
使用本地模型来影射或缓存远程模型有助于提供一个在所有时刻都可靠的、高响应能力的Swing组件。但这种方法的一个缺点,也是必要的代价是,模型数据不得不重复。另一个问题是,两个模型并非总是同步的,而且它们之间不得不用某种方法来互相协调。
在展开时更新:动态浏览

模型间的一种协调方法是仅在需要数据时更新本地模型,而非在此之前。这种技术在远程模型很慢或很大时是有用的,但这种技术要求模型基本上是静态的。DynamicTree用这种方法来浏览一个缓慢的静态树模型。
DynamicTree开始时是一个未展开的根节点,同时有一个展开监听器(expansion listener)等待用户输入来展开节点。当节点展开后,展开监听器启动一个SwingWorker线程。这个worker的construct()方法访问远程模型并返回新的子节点。然后worker的finished()方法会在事件派发线程中执行,将子节点添加到本地模型。
为简单起见,在同一时刻仅允许一个worker运行。如果有任何节点被折叠,当前活动的任何worker将被中断。(程序并不检查折叠的节点是否是worker正在展开的节点的祖先。)
执行顺序

图2表示了节点展开的过程。执行过程在图左边的事件派发线程中开始和结束。SwingWorker执行过程在图右边的worker线程中开始。实线箭头是方法调用,虚线箭头是返回,半箭头则是异步请求。

图2 DynamicTree执行顺序图
本节的余下部分将讨论类结构和实现。你可以也跳过下面的远程模型演示程序。后面的“下载”一节解释了如何下载和运行该演示程序。
实现

图3是一个概略的类结构图。

图3 DynamicTree各类
本地模型是一个包含DefaultMutableTreeNode节点的DefaultTreeModel。节点必须是可变的,以便能够动态地添加子节点。可变节点的userObject域可以用来指向远程模型。
TreeNodeFactory

远程模型实现了TreeNodeFactory接口:

public interface TreeNodeFactory {
  DefaultMutableTreeNode[] createChildren(Object userObject)
      throws Exception;
}

createChildren()在worker线程中被调用。类似于大多数异步和远程方法,它会抛出一个异常(一般是一个RemoteException或InterruptedException)。userObject参数是最近展开的节点指回远程模型的一个链接。一次返回一个包括所有子节点的数组比单独返回每个子节点更高效,还可以避免面对部分失败的情况。
在初始化时,每个子节点都要被设置allowsChildren属性和一个指回远程模型的链接。(如果远程节点是叶子节点,则allowsChildren属性设为false;否则allowsChildren被设为true以表明该节点可被展开。)
DefaultTreeNodeFactory是一个Adapter(请参考GOF的《Design Patterns》)。它将任意TreeModel配接成TreeNodeFactory接口。演示程序中用的是缺省树模型。在main方法中的一些注释掉的代码演示了如何安装一个FileSystemNodeFactory。SlowTreeNodeFactory是演示用的一个包装类;它在运行中插入随机的延迟。
未来的工作

我已经努力使DynamicTree保持简单。节点中除了节点的名字别无其他内容。在节点中需要填充内容的情况下,如果能异步地加载内容会好一些,比如你可能用一个树选择监听器(tree selection listener)来初始化加载过程。
下一节的远程表演示程序会更实用一些。

-------------------------------------------------------------------------------

远程表


假设你有一个带TableModel的JTable管理着一个远程设备,但接口在设备繁忙时慢如龟爬,你该怎么办?
下面的远程表演示程序使用后台线程和异步的回调操作(asynchronous callbacks)来显示或修改一个远程表模型。
图4是一个远程表编辑器正在向服务器发送新的值。未处理的单元格在更新完成前会保持黄色。

图4
RemoteTable bean

远程表演示程序用RMI实现,它由一个服务器和两个客户端(一个编辑器和一个查看器)组成。查看器实际上只是一个禁止了编辑功能的编辑器。
客户端使用一个RemoteTable组件bean,RemoteTable是设计成于远程表模型协作的JTable的子类。图4所示的编辑器由一个RemoteTable组件、一个状态标签、一个简单的活动计量器(右下角)和一些用于定位服务器的代码组成。
得到通知时更新

DynamicTree仅在需要数据时更新它的本地模型,与此不同的是,RemoteTable组件在一开始就获取所有的数据,然后监听此后的变化。基于通知(notification-based)的技术可以和动态模型一起工作,且在模型比较小时工作的很好。
对单元格的修改驱动通知。当用户完成对单元格的编辑,RemoteTable把编辑过的单元格标记为未处理的(黄色突出显示)并且安排一项SwingWorker任务。该woker的construct()方法会把新的值发送到远程模型。
当远程模型接收到新的值,它把变化通知给监听器。Worker的finished()方法的唯一功能是确认任务已完成;在来自远程模型的通知接受并处理完后,黄色的未处理单元格变回普通单元格。
任务队列

RemoteTable用一个QueuedExecutor调度它的SwingWorker任务,QueuedExecutor在单个线程中顺序执行所有的任务。(QueuedExecutor是Doug Lea的util.concurrent包的一部分,请参见后面的“参考资料”部分。)远程模型用RMI回调操作通知它的监听器。
为了支持可视的反馈,RemoteTable发送Task事件给注册的Task监听器。在任务进入调度时,监听器的taskStarted()被调用,taskEnded()在任务完成时被调用。客户端演示程序使用这些事件来启动或停止一个小动画并更新状态来。
执行次序

图5表示的是单元格的更新过程。执行过程的开始和结束都位于左边的事件派发线程。SwingWorker任务则在右边的executor的线程中执行。Worker的finished()方法的执行过程没有表示出来。

图5 RemoteTable执行顺序图
简化

简单起见,远程模型并没有对互相冲突的编辑提供保护。所以在同一时刻仅能有一个编辑器在运行。(并发编辑可以用添加请求ID(request IDs)的方法来实现。)
所作的另一项简化决定是客户端和服务器必须预先对表的列结构协商好。换句话说,服务器性客户端提供行数据,而客户端必须已经知道他们要处理的是什么表。演示用的客户端用DefaultModelTemplate来预先定义了各列的名称和类,来确定哪个单元格可以被编辑。(在演示程序中,前两列是不可编辑的。)
本节的余下部分阐述了类结构和实现。如果不想了解这个演示程序中所用的修订版的SwingWorker,你可以跳过去。“下载”一节解释了如何下载并运行这个演示程序。
实现

图6是一个概略的类结构图。

图6 RemoteTable各类
远程模型实现了RemoteTableModel接口,这个接口和AbstractTableModel很相似,除了它的所有方法都会抛出异常。要启动一个客户端,远程表模型发送一个完全更新事件给客户端已注册的的监听器。
RemoteTableModelAdapter配接任意的TableModel到一个RemoteTableModel。演示程序中的表模型取自The Java Tutorial,但插入了一些延迟来模拟实际情况。远程表模型事件包含已更新的单元格的值。
RemoteTable组件用一个DefaultRemoteTableModelListener来接受来自远程模型的回调。这个监听器会在事件派发线程中更新本地模型。因为远程模型可能要通知插入或删除某些行,所以监听器要求本地模型支持插入和删除操作,DefaultTableModel满足这个要求。

-------------------------------------------------------------------------------

SwingWorker修订版


演示程序用SwingWorker在后台执行费时的操作,然后更新UI。
这个演示程序所用的SwingWorker是基于《使用SwingWorker线程》文中提出的SwingWorker类,但重新实现了它以修正一处竞态条件,添加超时支持,和改进了异常处理。
这个新的实现还基于Doug Lea的util.concurrent包的FutureResult类(参见“参考资料”一节)。由于大量依赖了FutureResult所做的工作,SwingWorker类的实现是简单而灵活的。
本节的余下部分更详细地描述了实现的细节,请继续往下看或直接跳到后面下载源码。
Runnable FutureResult

FutureResult,正如它的名字所暗示的,它是用来保持某动作的结果的。它被设计成和一个Callable共同使用,Callable是一个会返回结果的runnable动作:

public interface Callable {
  Object call() throws Exception;
}

新的SwingWorker是一个Runnable FutureResult。在运行时,它把结果设成construct()的返回值,然后在事件派发线程中调用finished()方法。(注意:SwingWorker是一个抽象类;你要子类化它并实现construct()和finished()。)
下面的代码来自SwingWorker的run()方法:

  Callable function = new Callable() {
    public Object call() throws Exception {
      return construct();
    }
  };

  Runnable doFinished = new Runnable() {
    public void run() {
      finished();
    }
  };

  setter(function).run();
  SwingUtilities.invokeLater(doFinished);

第一段把construct()转换成一个Callable动作,第二段把finished()转换成作为Runnable的doFinished。然后setter(function)被运行,doFinished被调用。
setter(function)

上面缺少的部分是setter(function)。它创建一个刻板的Runnable。在运行时,这个Runnable调用参数指定的function,然后给结果设置返回值。下面是来自FutureResult的代码:

  public Runnable setter(final Callable function) {
    return new Runnable() {
      public void run() {
        try {
          set(function.call());
        }
        catch(Throwable ex) {
          setException(ex);
        }
      }
    };
  }

注意try-catch块所作的防护。如果construct()抛出任何东西(Exception、Error等等),都会被捕捉并记录下来。
不要抢跑:先construct,再start

调用start()来启动worker线程。这是修订版的SwingWorker和原来版本的一个重要区别。
在原来的版本中,SwingWorker()构造器自动启动线程,这种做法带来了一个线程和子类构造器竞争的危险:当SwingWorker()构造器已启动了线程,而子类的构造器还没完成。弥补方法是,先构造SwingWorker,然后再调用start()。
顺便一提,RemoteTable并不调用start()。正确来说,SwingWorker是作为一个Runnable被QueuedExecutor执行的。
超时支持

新的SwingWorker支持超时,这是通过覆盖getTimeout()方法已返回一个非零值来实现的。当超出超时时间,worker线程会被中断。
如果想查看使用超时的例子,请参阅注释版的getTimeout()方法和DynamicTree如何处理TimeoutException。
超时功能是用TimedCallable来实现的,其中使用了FutureResult的timedGet()方法。
增强的异常处理

construct()方法抛出的任何东西都会被记录。除了死循环和死锁,新的异常处理确保了SwingWorker处于“准备好”的状态。也就是说,它要么得到一个正确的结果,要么得到一个异常。
下面的get()方法用来取出结果。这个方法继承自FutureResult:

public Object get()
    throws InvocationTargetException, InterruptedException

如果construct()抛出一个Exception,get()方法就会抛出InvocationTargetException。要获得construct()方法实际上抛出的异常,可以调用getTargetException()。
如果取结果的线程在等待结果的过程中被中断,get()方法会抛出InterruptedException——但这种情况对SwingWorker来说很罕见,因为取结果的线程通常都是事件派发线程,并且在finished()会被调用以前,结果总是已经准备好的。
更多调用工具

SwingWorker的实现在jozart.swingutils包中。在同一个包里,你还能找到InvokeUtils类,这个类还提供了几个invokeXXX()方法。后台线程可以用这些方法来在事件派发线程中获取值和用户输入,再把结果返回到后台线程。

-------------------------------------------------------------------------------

下载


所有演示程序的源码,还有编译好的类文件和资源,都包括在这个zip文件中:threads3 demos.zip
threads3_demos.zip文件包括以下内容:
• jozart/
o dynamictree/ - DynamicTree 演示程序。
o remotetable/ - RemoteTable bean 源码。
o remotetabledemo/ - RemoteTable演示程序。
o swingutils/ - SwingWorker.
• EDU/ - util.concurrent 包(只包括了用到的类)。
• java.policy - RMI 安全策略文件(security policy)。
• remotetable.jar - RemoteTable bean (供各IDE使用)
注意:来自util.concurrent的类是从v1.2.3版选出的。只包括了演示程序中用到的类。
注意:运行这些演示程序需要Java 2。(util.concurrent包有几处用到了Collections。)
运行演示程序,首先请解压threads3_demos.zip到一个空文件夹,注意不要改变里面文件夹的名字。然后换到你把文件解压到的那个文件夹。
运行DynamicTree

将DynamicTree作为applet运行:
> appletviewer jozart/dynamictree/DynamicTree.html
将DynamicTree作为application运行:
> java jozart.dynamictree.DynamicTree
运行RemoteTableDemo

远程表服务器和客户端是设计成用RMI分别运行的。但RMI可能难以设置。RMI要求一个RMI registry、一个HTTP服务器和一个安全策略文件。幸运的是,另一个演示程序不那么麻烦,我会先解释怎样运行它。
RemoteTableDemo把一个编辑器、一个查看器和一个服务器都包到了一起。
将RemoteTableDemo作为applet运行:
> appletviewer jozart/remotetabledemo/RemoteTableDemo.html
将RemoteTableDemo作为application运行:
> java jozart.remotetabledemo.RemoteTableDemo
分别运行服务器和客户端

要分别运行服务器和客户端,需要执行以下步骤:
1. 确定rmiregistry正在运行:
> start rmiregistry
2. 确定有一个http服务器正在运行:
> start httpd
3. 换到演示程序的目录。
> cd threads3
4. 启动服务器和客户端:
> java -Djava.security.policy=java.policy
-Djava.rmi.server.codebase=http://host/threads3/
jozart.remotetabledemo.RemoteTableServer
> java -Djava.security.policy=java.policy
-Djava.rmi.server.codebase=http://host/threads3/
jozart.remotetabledemo.RemoteTableEditor
> java -Djava.security.policy=java.policy
-Djava.rmi.server.codebase=http://host/threads3/
jozart.remotetabledemo.RemoteTableViewer
你需要在codebase设置中设置你的正确的主机名。你还应该检查java.policy没有给出过多的权限。
更多关于RMI的介绍请参阅The Java Tutorial(参见“参考资料”一节)。

------------------------------------------------------------------------------

结论


本文演示了两种共同使用线程和基于模型的组件(如JTable和JTree)的方法。并提供了一个修订过的SwingWorker工具类。
在一开头,我说明了构建线程安全的组件是困难的。Java在语言级别提供了对线程和锁的支持,但这并不能使得并发编程突然变得容易起来。Swing的单线程规则使得程序员们的日常任务从线程安全的复杂性中解脱出来,但当确实需要线程时却成了阻碍。
为了使得我自己和我的代码的未来使用者在并发编程上的工作更容易些,我尽可能地使用了工具包、证明有效的设计模式和广泛接受的约定。
在文章的最后,我要给任何有兴趣扩展SwingWorker或者开发他们自己的线程工具的读者提个建议:弄清楚你的内存模型。
关于共享资源的访问控制,我还要再说一下。Java的synchronized语句确保了在线程间传递值的安全。这个重要的细节常常被程序员们忽略。关于Java内存模型的更多信息可以参考Doug Lea对《Concurrent Programming in Java》一书的在线补充。这个参考资料和其他参考资料的链接都列在了下一节。

-------------------------------------------------------------------------------

参考资料


1. Threads and Swing - Hans Muller and Kathy Walrath.
2. Using a Swing Worker Thread - Hans Muller and Kathy Walrath.
3. Understanding the TreeModel - Eric Armstrong, Tom Santos, Steve Wilson.
4. The JTable Class is DB-Aware - Philip Milne and Mark Andrews.
5. The Java Tutorial on RMI - Ann Wollrath and Jim Waldo.
6. Concurrent Programming in Java - Doug Lea.
7. util.concurrent package - Doug Lea.

-------------------------------------------------------------------------------

关于作者


Joseph Bowbeer是住在西雅图的一个Java顾问。他对并发编程的着迷使他找准了自己的方向。他要感谢Doug Lea、Tom May、David Holmes和Joshua Bloch,感谢他们分享他们的知识。文中的任何错误和疏忽都归咎于他自己。

 

 

版权所有:UML软件工程组织