Java多線程開發系列之番外篇:事件派發線程

事件派發線程是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 }

轉自:http://www.knowsky.com/889791.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章