在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的事件队列就类似于上述的事件队列(基本原理相似,但是Swing内部实现会做些优化),说它是单线程图形工具包指的是仅有单一消费者,也就是常说的事件分发线程(EDT),一般来讲,除非你的应用程序停止,否则EDT会永不间断地徘徊在处理请求与等待请求之间。
下图是Swing事件队列的实现机制:
很显然,如果在加工某一个货物上花费很长的时间,那么后续的货物只好等待。
对于单一线程的事件队列来说有两个非常突出的特性:
EDT要处理所有GUI操作,它是职责分明且非常忙碌的。也就是说你要记住两条原则:
上面说过Swing不是一个“安全线程”的API,为什么要这样设计?再回看上图就会明白:Swing的线程安全不是靠自身组件的API来保障,虽然repaint方法是这样,但是大多数SwingAPI是非线程安全的,也就是说不能在任意地方调用,它应该只在EDT中调用。Swing的线程安全靠事件队列和EDT来保障。
由于Swing自身不是线程安全,如果你在其他线程访问和修改GUI组件,那么你必须要使用SwingUtilities. invokeAndWait(runnable), SwingUtilities. invokeLater(runnable)。也就是说对非EDT的并发调用需通过invokeLater()和invokeAndWait()使请求插入到队列中等待EDT去执行。
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