在Swing程序中,经常能看到如下这种代码:
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run() {
textField1.setText("element changed!");
textField1.setForeGround(Color.RED);
}
});
为什么要用SwingUtilities.invokeLater,而不直接调用呢?因为大多数SwingAPI是非线程安全的,也就是说不能在任意地方调用,它应该只在EDT中调用。Swing的线程安全靠事件队列和EDT来保障。
EventQueue的派发机制由单独的一个线程管理,这个线程称为事件派发线程(EDT)。和其他很多桌面API一样,Swing将GUI请求放入一个事件队列中执行。
通过EDT,使得不具备线程安全的Swing函数库避开了并发访问的问题。
背景概念
要了解EDT,首先需要了解一些背景概念:
同步与异步:
同步是程序在发起请求后开始处理事件并等待处理的结果或等待请求执行完毕,在此之前程序被block住直到请求完成。
异步是当前程序发起请求后立即返回,当前程序不会立即处理该事件并等待处理的结果,请求是在稍后的某一时间才被处理。
串行与并行:
串行是指多个要处理请求顺序执行,处理完一个再处理下一个;
并行可以理解为并发,是同时处理多个请求(实际上我们只能理解为是这样,特别是CPU数目少于线程数的机器而言,真正意义的并发是不存在的,各个线程只是断断续续地交替地执行)。
下图演示了串行与并行的机制。可以这么说,在引入多线程之前,对于同一进程或者程序而言执行的都是串行操作。
串行:
并行:
生产者/消费者模式:
可以想象这样一副场景,某车间的一条传送带,有一个或多个入口不断产生待加工的货物,这种不断产生货物的称为生产者;传送带的末端是一个或多个工人在加工货物,称作消费者。有时由于传送带上没有足够的货物使得某一工人暂时空闲,有时又由于部分货物需加工的时间较长出现传送带上待加工的货物堆积。
如果用Java实现一个简单的生产者消费者模型,利用线程的等待/通知机制很容易实现。
public class SyncQueue {
private List buffer = new ArrayList();
//消费
public synchronized Object pop() {
Object e;
while (buffer.size() == 0) {
try {
wait();
} catch (InterruptedException e1) {
// ignore it
}
}
e = buffer.remove(0);
return e;
}
//生产
public synchronized void push(Object e) {
notifyAll();
buffer.add(e);
}
}
事件队列:
在计算机数据结构中,队列是一个特殊的数据结构。其一、它是线性的;其二、元素是先进先出的,也就是说进入队列的元素必须从末端进入,先入队的元素先得到执行,后入队的元素等待前面的元素执行完毕出队后才能执行,队列的处理方式是执行完一个再执行下一个。
队列与线程安全是两个不同的概念,如果要将队列加上线程安全的特性,只需要仿照上述生产者/消费者加上线程的等待/通知即可。
Swing程序中的线程
一个Swing程序中一般有下面三种类型的线程:
初始化线程(Initial Thread) - UI事件调度线程(EDT)
- 任务线程(Worker Thread)
初始化线程:每个程序必须有一个main方法,这是程序的入口。该方法运行在初始化或启动线程上。初始化线程读取程序参数并初始化一些对象。在许多Swing程序中,该线程主要目的是启动程序的图形用户界面(GUI)。创建UI的点,也就是程序开始将控制权转交给UI时的点。一旦GUI启动后,对于大多数事件驱动的桌面程序来说,初始化线程的工作就结束了。
EDT:Swing程序只有一个EDT,该线程负责GUI组件的绘制和更新,通过调用程序的事件处理器来响应用户交互。所有事件处理都是在EDT上进行的,程序同UI组件和其基本数据模型的交互只允许在EDT上进行,所有运行在EDT上的任务应该尽快完成,以便UI能及时响应用户输入。
- 从其他线程访问UI组件及其事件处理器会导致界面更新和绘制错误。——必须通过EDT刷新组件
- 在EDT上执行耗时任务会使程序失去响应,这会使GUI事件阻塞在队列中得不到处理。——不能在EDT中执行其他耗时操作
- 应使用独立的任务线程来执行耗时计算或输入输出密集型任务,比如同数据库通信、访问网站资源、读写大树据量的文件。——耗时操作应放到独立的任务线程中,通过SwingWorker启动
Swing事件分发线程
Swing的事件队列就类似于上述的事件队列(基本原理相似,但是Swing内部实现会做些优化),说它是单线程图形工具包指的是仅有单一消费者,也就是常说的事件分发线程(EDT),一般来讲,除非你的应用程序停止,否则EDT会永不间断地徘徊在处理请求与等待请求之间。
下图是Swing事件队列的实现机制:
很显然,如果在加工某一个货物上花费很长的时间,那么后续的货物只好等待。
对于单一线程的事件队列来说有两个非常突出的特性:
- 将同步操作转为异步操作。
- 将并行处理转换为串行顺序处理。
EDT要处理所有GUI操作,它是职责分明且非常忙碌的。也就是说你要记住两条原则:
- 职责分明,任何GUI请求都应该在EDT中调用。
- 需要处理的GUI请求非常多,包括窗口移动、组件自动重绘、刷新,它很忙,所以任何与GUI无关的处理不要由EDT来负责,尤其是I/O这种耗时的操作。
上面说过Swing不是一个“安全线程”的API,为什么要这样设计?再回看上图就会明白:Swing的线程安全不是靠自身组件的API来保障,虽然repaint方法是这样,但是大多数SwingAPI是非线程安全的,也就是说不能在任意地方调用,它应该只在EDT中调用。Swing的线程安全靠事件队列和EDT来保障。
invokeLater和invokeAndWait
由于Swing自身不是线程安全,如果你在其他线程访问和修改GUI组件,那么你必须要使用SwingUtilities. invokeAndWait(runnable), SwingUtilities. invokeLater(runnable)。也就是说对非EDT的并发调用需通过invokeLater()和invokeAndWait()使请求插入到队列中等待EDT去执行。
- invokeLater(runnable)方法是异步的,它会立即返回,具体何时执行请求并不确定,所以命名invokeLater是稍后调用。
- invokeAndWait(runnable)方法是同步的,它被调用结束会立即block当前线程(调用invokeAndWait的那个线程)直到EDT处理完那个请求。invokeAndWait一般的应用是取得Swing组件的数据。
invokeAndWait有非常重要的一条准则是:它不能在EDT中被调用,否则程序会抛出Error,请求也不会去执行。看源码:
public static void invokeAndWait(Runnable runnable)
throws InterruptedException, InvocationTargetException {
//不能在EDT中调用invokeAndWait
if (EventQueue.isDispatchThread()) {
throw new Error("Cannot call invokeAndWait from the event dispatcher thread");
}
class AWTInvocationLock {}
Object lock = new AWTInvocationLock();
InvocationEvent event =
new InvocationEvent(Toolkit.getDefaultToolkit(), runnable, lock,
true);
synchronized (lock) {
//添加进事件队列
Toolkit.getEventQueue().postEvent(event);
//block当前线程
lock.wait();
}
Throwable eventThrowable = event.getThrowable();
if (eventThrowable != null) {
throw new InvocationTargetException(eventThrowable);
}
}
如果invokeAndWait在EDT中调用,那么首先将请求压进队列,然后EDT便被block,等待请求结束通知它继续运行。
而实际上请求将永远得不到执行,因为它在等待队列的调度使EDT执行它,这就陷入一个僵局:EDT等待请求先执行,请求又等待EDT对队列的调度。彼此等待对方释放锁是造成死锁的四类条件之一。Swing有意地避免了这类情况的发生。
参考:
深入浅出Swing事件分发线程: http://space.itpub.net/13685345/viewspace-374940
使用SwingWorker: http://blog.sina.com.cn/s/blog_4b6047bc010007so.html