Swing 的任务线程与 EDT 事件分发队列模型
1 现象及问题
在Swing程序中,经常能看到如下这种代码:
为何用invokeLater,而不直接调用呢?
大多数Swing的API非线程安全,不能在任意地方调用,应该只在EDT中调用。
Swing的线程安全靠事件队列和EDT来保证。
EventQueue的派发机制由单独的一个线程 - 事件派发线程(EDT)管理。
Swing将GUI请求放入一个事件队列中执行。通过EDT,使得非线程安全的Swing函数库避开了并发问题。
3 Swing 中的线程分类
一个Swing程序中一般有下面三种类型的线程:
-
初始化线程(Initial Thread)
每个程序必须有一个main方法作为程序的入口。
该方法运行在初始化或启动线程上。初始化线程读取程序参数并初始化一些对象。
在许多Swing程序中,该线程主要目的是启动程序的GUI。创建UI的点,也就是程序开始将控制权转交给UI时的点。
一旦GUI启动后,对大多数事件驱动的桌面程序,初始化线程的工作就结束了。 -
UI事件调度线程(EDT)
Swing程序只有一个EDT,负责GUI组件的绘制和更新,调用程序的事件处理器来响应用户交互。
所有事件处理都是在EDT执行,程序同UI组件和其基本数据模型的交互只允许在EDT上进行。
所有运行在EDT上的任务应该尽快完成,以便UI能及时响应用户输入。 -
任务线程(Worker Thread)
4 Swing 编程铁律
4.1 必须通过EDT刷新组件
从其他线程访问UI组件及其事件处理器会导致界面更新和绘制错误
4.2 禁止在EDT执行其他耗时操作
在EDT上执行耗时任务会使程序失去响应,这会使GUI事件阻塞在队列中得不到处理
4.3 耗时操作放在独立的任务线程
通过SwingWorker启动。应使用独立的任务线程来执行耗时计算或输入输出密集型任务。
- 比如同数据库通信
访问网站资源、读写大树据量的文件。
任何干扰或延迟UI事件的处理只应出现在独立任务线程中。
- 在初始化线程(即禁止在main方法中直接创建Frame,在初始化线程中应使用invokeLater初始化GUI)
- 任务线程同Swing组件或其缺省数据模型进行的交互
都是非线程安全性操作。
通过SwingWorker类的管理,隔离EDT和任务线程,使它们各负其责
- EDT 绘制和更新界面,并响应用户输入
- 任务线程,执行和界面无直接关系的耗时任务和I/O密集型操作
5 事件队列
在计算机数据结构中,队列是一个特殊的数据结构。
- 它是线性的
- 元素是先进先出的,进入队列的元素必须从末端进入,先入队的元素先得到执行,后入队的元素等待前面的元素执行完毕出队后才能执行,队列的处理方式是执行完一个再执行下一个
队列与线程安全是无关的,不过要想将队列保证线程安全,只需要仿照生产者/消费者模式加上线程的等待/通知即可。
6 Swing 事件分发线程(EDT)
Swing的事件队列就类似事件队列,仅单一消费者,即一个事件分发线程。
除非你的程序停止,否则EDT会永不间断地徘徊在处理请求与等待请求之间。
- Swing事件队列的实现机制图解
6.1 单一线程的事件队列的特性
- 将同步操作转为异步操作
- 将并行处理转换为串行顺序处理
6.2 EDT要处理所有GUI操作
- 职责明确,任何GUI请求都应该在EDT中调用
- 要处理的GUI请求非常多,包括窗口移动、组件自动重绘、刷新,它很忙。任何与GUI无关的处理不要由EDT执行,尤其是I/O耗时操作
7 Swing不是一个“安全线程”的API,为什么要这样设计
Swing的线程安全不是靠自身组件的API来保障,虽然repaint方法是这样,但是大多数SwingAPI是非线程安全的,也就是说不能在任意地方调用,它应该只在EDT中调用。Swing的线程安全靠事件队列和EDT保证。
8 invoke系方法
对非EDT的并发调用需通过invokeLater()和invokeAndWait()使请求插入到队列中等待EDT去执行。
由于Swing本身非线程安全,如果你在其他线程访问和修改GUI组件,必须使用
8.1 SwingUtilities. invokeAndWait(runnable)
同步的,它被调用结束会立即阻塞当前线程,直到EDT处理完该请求。
一般用于取得Swing组件的数据。
8.2 SwingUtilities. invokeLater(runnable)
使 doRun.run() 在AWT事件分法线程上异步执行。所有待处理的AWT事件被执行后,就会发生这种情况。当应用程序线程需要更新GUI时,应使用此方法。
在下面的示例中,invokeLater调用将Runnable对象doHelloWorld排队在事件分配线程上,然后打印一条消息。
Runnable doHelloWorld = new Runnable() { public void run() { System.out.println("Hello World on " + Thread.currentThread()); }
};
SwingUtilities.invokeLater(doHelloWorld);
System.out.println("This might well be displayed before the other message.");
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
如果从事件分发线程(例如,从JButton的ActionListener)调用invokeLater,则 doRun.run 仍将延迟,直到处理完所有未决事件。请注意,如果doRun.run 引发未捕获的异常,则事件分发线程将展开(而不是当前线程)。
从1.3版本开始,此方法只是java.awt.EventQueue.invokeLater()的封面。
与Swing的其余部分不同,可以从任何线程调用此方法。
- 准则
不能在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); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
如果invokeAndWait在EDT中调用,那么首先将请求压进队列,然后EDT便被block,等待请求结束通知它继续运行。
而实际上请求将永远得不到执行,因为它在等待队列的调度使EDT执行它,这就陷入一个僵局:EDT等待请求先执行,请求又等待EDT对队列的调度。彼此等待对方释放锁是造成死锁的四类条件之一。Swing有意地避免了这类情况的发生。
文章来源: javaedge.blog.csdn.net,作者:JavaEdge.,版权归原作者所有,如需转载,请联系作者。
原文链接:javaedge.blog.csdn.net/article/details/105700087
- 点赞
- 收藏
- 关注作者
评论(0)