事件派发线程是java Swing开发中重要的知识点,在安卓app开发中,也是非常重要的一点。今天我们在多线程开发中,穿插进来这个线程。分别从线程的来由、原理和使用方法三个方面来学习事件派发线程。
一、事件派发线程的前世今生
事件(Event)派发(Dispatch)线程(Thread)简写为EDT,也就是各个首字母的简写。在一些书或者博客里边也将其译为事件分发线程、事件调度线程。巴拉巴拉,总之,知道这些名字就行。笔者认为这里翻译成派发更准确点。
熟悉Swing和awt编程的小伙伴对事件派发线程应该都不陌生。如果你提反对意见的话,只能说明你对Swing和awt编程还不够熟悉。
事件派发线程诞生的故事背景是这样的:
界面上各个控件对象都有保存自己的数据变量。如果出现多线程操作就会出现很多问题,诸如数据变脏,数组越界,空引用等等问题。
举个栗(例)子
线程A发现panel中还有数据要显示(check data),于是调用滚动条向下滚动。这时,panel内部要调用数据中为展示的数据用来显示。可是在展示的过程中,线程发生了切换。由其它线程B删掉了需要展示的数据,这时线程A再次被唤醒继续运行,显示接下来的内容。由于已经过了Check Data的逻辑。所以接下来就要调用已经不存在的数据用来展示。最后就会出现各种奇怪的问题。(如果你没看懂,就理解成各个线程最终都在操作控件的数据源,则控件在显示的时候就可能会出现异常)。
通常来说解决这种多线程冰法问题方式就是"锁"或者"同步"。
当时Sun公司的Swing小组最初也是这个思路,但是让Swing小组最终改变主意的由于接下来的两个原因:
1、数据同步在保证线程安全的同时,很耗费时间。UI最重要的就是界面响应速度,毕竟谁也不想面对一个幻灯片在操作。
2、Swing小组调查了其他小组在线程安全的用户界面工具包方(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )面的经验后,发现结果并不是那么的美好:开发线程安全包的工程师被各种同步操作搞晕了头,程序经常发生死锁。
就此,Swing小组决定使用单一线程来控制整个界面的控件绘制。这个线程就是事件派发线程。
事件派发线程就是这样被创造出来的:
二、事件派发线程的原理
事件派发线程的原理其实非常的简单,在界面后台始终只有这一个线程在工作,这个线程就是事件派发线程。当你有需要操作界面的行为时,将这些行为添加到事件派发线程的事件队列中,事件派发线程会依次执行这个队列中的请求。
这有点像单核cpu进行多线程操作的场景,不同的地方是,这时候事件派发线程的作用是单核cpu。
具体内容可以查看下图(图片来源于网络)
各个线程将GUI请求排成队列,然后由事件派发线程依次执行这个队列中的请求。
如果从设计模式的角度来看,这个地方是一个典型的"消费者"模式,有兴趣的小伙伴可以查阅下相关的设计模式内容,这里就不展开赘述了。
了解了事件派发线程原理之后,我们会发现这样一个问题:
eventQueue中的事件没有轻重缓急之分,是遵循FIFO的原则的。那么如果前边的请求非常耗时,需要大量的db请求、IO等操作,那么后边的请求只能一直等待。
当初舍弃‘同步‘是为了快,现在界面还是会卡死,违背了初衷。
基于以上,Swing开发人员提出了两点在使用事件派发线程时需要遵守的原则:
1、只有事件派发线程可以调用Swing组件,其他线程都离组件远远的。(有些地方称这条准则为单一线程规则single-thread rule)
2、如果某一个GUI请求非常耗时,就不要把这个请求发送给事件派发线程。直到这个请求通过其他线程处理之后,最后的少部分界面请求再发送给事件派发线程。
三、怎么使用事件派发线程
上面说了非常多,但是不知道怎么使用事件派发线程,则上边所述也就没有什么用了。
首先,前文中提到了事件派发线程是启动GUI后,(其实这里还存在有一个初始化线程,短暂的启动GUI的生命过程)系统自动启动的一个线程。
所以我们就不用手动创建和运行线程了。我们要做的就(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )是向事件派发线程中添加各种GUI请求到eventQUEUE中去即可。
Swing为我们提供了三个常用的API
1 SwingUtilities.invokeAndWait(Runnable runnable)//同步请求,发送请求的线程会一直等到EDT执行完毕自己的请求后,才会继续执行剩余代码; 2 3 SwingUtilities.invokeLater(Runnable runnable)//异步请求,发送请求的线程在请求添加到EDT的eventQUEUE后,才会执行剩余代码; 4 5 SwingUtilities.isEventDispatchThread()//判断当前线程是否为事件派发线程。
一般来说在编写请求代码的时候,最好先判断下执行线程是否为事件派发线程,然后在选择是直接执行还是添加到事件队列中。
值得注意的是这里会存在一个问题:
就是如果当前线程就是事件派发线程时,是不允许其执行invokeAndWait()同步方法的。
这是由于如果出现这种情况EDT就会停顿(wait)在这个点,等待EDT去执行添加的请求,同时由于EDT已经停顿在了这个点,那么EDT也就不会去处理eventQUEUE中的请求,形成了一种死锁。
好在JDK中已经对这种情况做了校验,所以上面没太看懂的同学无需太在意,只要记住结果即可:
最后我们再来一个实际工作中代码的例子
1 if(SwingUtilities.isEventDispatchThread()) 2 { 3 OptTree.RefreshNode(); 4 } 5 else 6 { 7 SwingUtilities.invokeAndWait(new Runnable() 8 { 9 @Override 10 public void run() 11 { 12 OptTree.RefreshNode(); 13 } 14 }); 15 }