深入淺出Swing事件分發線程

《FilthyRichClients》讀書筆記-SwingのEDT

《FilthyRichClients》讀完了前幾個章節,現將我的體會結合工作以來從事Swing桌面開發的經驗,對本書的一些重要概念進行一次分析,對書中的一些遺漏與模糊的地方及時補充,同時使讀者消除長期以來“Swing性能低、界面醜陋”諸如此類的舊觀念。讀書筆記僅談談我對Swing的理解,難免會犯錯誤,還望廣大讀者指教。

    書中第二章-Swing渲染基本原理 中對Swing的線程做了系統地介紹。相比其他同類Swing教程,已經講得非常深入了。但是如果讀者之前對線程的掌握程度有限,尤其是編寫代碼比較隨意的coder們,動輒就大量編寫類似下面這樣的代碼:
jButton1.addActionListener(new ActionListener(){
   public void actionPerformed(ActionEvent e) {
    // TODO
   }
  });
這樣的代碼可能是netBeans這樣的工具生成的“傑作”。但是如果這個人再懶惰一點,可能會直接在TODO下面寫上長長一堆代碼,還伴隨着不可預知的 I/O操作,很多人指責界面被僵住是Swing性能的問題。在新式的JDK中,Swing已經在性能方面改進了很多,完全可以這麼說:與應用程序自身的業務計算相比,界面上的耗時可以忽略。但是如果上述惡習改不掉的話,Swing永遠“快”不起來,SWT也同樣如此,因爲它們都是單線程圖形工具包。
    書上有這樣一段話:“EventQueue的派發機制由單獨的一個線程管理,這個線程稱爲事件派發線程(EDT)”。和其他很多桌面API一樣,Swing將GUI請求放入一個事件隊列中執行。如果不明白什麼是事件隊列、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);
 }
}
在JDK 5中新出現了許多具有併發性的數據結構在java.util.concurrent包中,它們適合於特殊的場合,本帖不作解釋。

    事件隊列:在計算機數據結構中,隊列是一個特殊的數據結構。其一、它是線性的;其二、元素是先進先出的,也就是說進入隊列的元素必須從末端進入,先入隊的元素先得到執行,後入隊的元素等待前面的元素執行完畢出隊後才能執行,隊列的處理方式是執行完一個再執行下一個。隊列與線程安全是兩個不同的概念,如果要將隊列加上線程安全的特性,只需要仿照上述生產者/消費者加上線程的等待/通知即可。

而Swing的事件隊列就類似(基本原理相似,但是Swing內部實現會做些優化)於上述的事件隊列,說它是單線程圖形工具包指的是僅有單一消費者,也就是常說的事件分發線程(EDT),一般來講,除非你的應用程序停止,否則EDT會永不間斷地徘徊在處理請求與等待請求之間。下圖是Swing事件隊列的實現機制:



很顯然,如果在加工某一個貨物上花費很長的時間,那麼後續的貨物只好等待。對於單一線程的事件隊列來說有兩個非常突出的特性:一、將同步操作轉爲異步操作。二、將並行處理轉換爲串行順序處理

如果你能理解上述圖,那麼你就應該意識到:EDT要處理所有GUI操作,它是職責分明且非常忙碌的。也就是說你要記住兩條原則:一、職責分明,任何GUI請求都應該在EDT中調用。二、需要處理的GUI請求非常多,包括窗口移動、組件自動重繪、刷新,它很忙,所以任何與GUI無關的處理不要由EDT來負責,尤其是I/O這種耗時的操作。
    書中還講到Swing不是一個“安全線程”的API,爲什麼要這樣設計,再回看上圖就會明白:Swing的線程安全不是靠自身組件的API來保障,雖然repaint方法是這樣,但是大多數Swing API是非線程安全的,也就是說不能在任意地方調用,它應該只在EDT中調用。Swing的線程安全靠事件隊列和EDT來保障。
    invokeLater和invokeAndWait:前文提到,Swing自身不是線程安全,對非EDT的併發調用需通過 invokeLater(runnable)和invokeAndWait(runnable)使請求插入到隊列中等待EDT去執行。 invokeLater(runnable)方法是異步的,它會立即返回,具體何時執行請求並不確定,所以命名invokeLater是稍後調用。invokeAndWait(runnable)方法是同步的,它被調用結束會立即block當前線程(調用invokeAndWait的那個線程)直到EDT處理完那個請求。invokeAndWait一般的應用是取得Swing組件的數據,例如取得JSlider組件的當前值:
public class Task implements Runnable {
 private JSlider slider;
 private int value;
 public Task() {
  //slider = ...;
 }
 @Override
 public void run() {
  try {
   Thread.sleep(1000); // 有意停住1秒
  } catch (InterruptedException e) {
  }
  value = slider.getValue();
 }
 public int getValue() {
  return value;
 }
}
而外部非EDT線程可以這樣調用:
Task task = new Task();
  try {
   EventQueue.invokeAndWait(task);
  } catch (InterruptedException e) {
  } catch (InvocationTargetException e) {
  }
  int value = task.getValue();
當線程運行到EventQueue.invokeAndWait(task)時會立即被block至少1秒,待invokeAndWait返回時已經可以安全地取到值了。invokeAndWait被這樣命名也反映了使用的意圖:調用並等待結果。invokeAndWait有非常重要的一條準則是它不能在 EDT中被調用,否則程序會拋出Error,請求也不會去執行。

public static void invokeAndWait(Runnable runnable)
             throws InterruptedException, InvocationTargetException {

        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); //添加進事件隊列
            lock.wait(); // block當前線程
        }

        Throwable eventThrowable = event.getThrowable();
        if (eventThrowable != null) {
            throw new InvocationTargetException(eventThrowable);
        }
    }


爲什麼要有這樣一條限制?結合前文不難得出-防止死鎖。如果invokeAndWait在EDT中調用,那麼首先將請求壓進隊列,然後EDT便被 block(因爲它就是調用invokeAndWait的當前線程)等待請求結束通知它繼續運行,而實際上請求將永遠得不到執行,因爲它在等待隊列的調度使EDT執行它,這就陷入一個僵局-EDT等待請求先執行,請求又等待EDT對隊列的調度。彼此等待對方釋放鎖是造成死鎖的四類條件之一。Swing有意地避免了這類情況的發生。

    書中也提到了同步的繪製請求,作爲隊列,一條基本原則就是先進先出。那麼paintImmediately到底是怎樣的呢?顯然這個調用請求不會稍後去執行,也就是說不會插入到隊列的末尾等到排在它前面的請求執行完再去執行它,而是“破壞”順序性原則優先去執行,前面提到,Swing的事件隊列相對基礎的同步隊列做了很多優化,那麼這麼說它是否被插入到隊列最前面呢,也就是0這個位置?貌似也不是,書上說“已經在EDT中調用的方法中間...”,那麼就是比當前正在處理的繪製請求還要優先,因爲它是當前繪製請求的一部分,所以當前繪製請求(EDT正在處理的那個請求)要等它處理完成後再繼續處理。(好好體會吧)
    SwingWorker:推薦一篇Blog,http://blog.sina.com.cn/s/blog_4b6047bc010007so.html,作者是原Sun中國工程研究院的陳維雷先生,他對Swing的造詣非淺,他的Blog中有3篇介紹這一主題的文章,詳盡程度要比該書詳細得多。

    最後,談一下理解EDT對設計模式的幫助。通過上述對事件隊列和EDT的分析,有這樣一種體會:事件隊列是一個非常好的處理併發設計模型,不僅 Swing用它來處理後臺,Java的很多地方都在用,只不過對於處理服務器端的併發請求有多個處理線程在等候處理請求,也就是常說的線程池。而對於單用戶的桌面應用,單線程調用要比多現成API更簡單,“Swing後臺這樣做是爲了保證事件的順序可預見性”,而且相對於服務器,客戶端桌面層的請求要少得多,所以單線程就足夠應對了。
單一Thread化的訪問

通過EDT,使得不具備線程安全的Swing函數庫避開了併發訪問的問題。如果你也有一個不具備thread安全性的函數庫並想在multithreaded環境下使用應該怎麼辦?只要你是從單一的thread來訪問這個函數庫,程序就不會遭遇到任何數據同步的問題。

發佈了4 篇原創文章 · 獲贊 3 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章