此文被笔者收录在系列文章 架构师必备(系列) 中
GUI程序任务必须运行在Swing的事件线程中,几乎所有的GUI工具都实现为单线程化子系统,意味着所有的GUI的活动都被限制在一个单独的线程中。
一、GUI单线程化细节
现在的GUI框架会创建一个专门的线程事件派发线程(EDT)用来处理GUI事件。多线程的GUI框架容易受死锁的影响,原因在于:
1、事件冒泡的传递机制,如果是双向的就有可能产生dead lock。
2、MVC模式不一致的锁顺序再次伴随着死锁的风险一同到来。
Swing中的线程限制
AWT和Swing提供的事件处理工具在结构上类似于单例的Executor。只有唯一的一个线程在处理GUI任务,一个任务结束才开始下一下,不会让两个任务交迭,这也是开发GUI程序重要的一点。 所有的Swing组件和数据模型都被限制在事件线程中,GUI对象不用同步,仅仅依靠线程限制来保持一致性。
Swing的组件和模型只能在事件分派线程中被创建、修改和请求。你无法从事件线程之外的地方访问表现对象,也有一些例外,如SwingUtilities.invokeLater等全局方法。下面是用Executor来模拟swing线程封闭的一个例子
public class SwingUtilities {private static final ExecutorService exec = Executors.newSingleThreadExecutor(new SwingThreadFactory());
private static volatile Thread swingThread;
private static class SwingThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
swingThread = new Thread(r);
return swingThread;
}
}
public static boolean isEventDispatchThread() {
return Thread.currentThread() == swingThread;
}
public static void invokeLater(Runnable task) {
exec.execute(task);
}
public static void invokeAndWait(Runnable task)
throws InterruptedException, InvocationTargetException {
Future f = exec.submit(task);
try {
f.get();
} catch (ExecutionException e) {
throw new InvocationTargetException(e);
}
}
}
二、GUI任务执行
- 短期任务:可以全部留在事件线程中完成,事件起源于事件线程,冒泡似的传递到应用程序提供的监听器。
- 耗时任务:必须在事件线程以外的线程中运行它,这样才能把控制权尽快交还给事件线程,避免UI冻结。在执行完成后必须向事件线程提交另一个任务来更新状态。
public void actionPerformed(ActionEvent e) {
button.setEnabled(false);
label.setText("busy");
exec.execute(new Runnable() {
public void run() {
try {
/* Do big computation */
} finally {
GuiExecutor.instance().execute(new Runnable() {
public void run() {
button.setEnabled(true);
label.setText("idle");
}
});
}
}
});
}
});
取消
可以通过线程中断来实现取消操作,也可以用Future的cacnel,并设置mayInterruptIfRunning参数为ture来中断一个执行运行中的任务的线程。同样也需要限定在事件线程中,限制在事件线程中其实就是限制在ActionListener适配器中。
进度和完成标识
FutureTask有一个done钩子函数,可以方便任务完成后的通知,后台Callable完成生,会调用done。这里我们可以参考BackgroundTask类的实现来完成这个功能。
三、共享数据模型
GUI程序中,除了事件线程外,唯一的线程就只有主线程,强制这些程序遵守单线程规则很容易,不要在主线程中访问数据模型和视图组件。只要阻塞不过度影响响应性,多线程操作数据的问题可以通过线程安全的数据模型来解决,如果数据模型支持精细的并发,事件线程和后台线程就能共享它,而且不存在响应性的问题。不利的一面是,不能提供一个数据快照,线程安全的数据模型必须在更新时生成事件,才能在数据变化后更新视图。
有时使用版本化数据模型,比如copy-on-write容器的迭代器。如果一个数据模型必须被多个线程共享,而且实现一个线程安全模型的尝试却由于阻塞,一致性或复杂度等原因而失败,这时可以考虑采用分拆模型设计。
应用程序的数据模型即包含表现域,又包含应用域,我们称这种应用程序是分拆模型的设计。SwingWorker类是Swing的一个辅助功能,可以用来参考,包括上面的进度和完成标识功能。