標籤:style http color io java ar for 資料 div
原文地址
Swing程式的線程處理
前言
因為很多人會見到一些原始碼中調用SwingUtilities的invokeLater或者invokeAnd-Wait方法,但是卻不理解它們到底起到了什麼作用,本文的目標就是讓你理解這兩個方法的意義。本文是swing編程基礎且不可越過的一節。而且,如你所見,本文的副標題是“Swing程式的線程處理”,其實這是對本文內容更好的概括。
事件派發線程(EDT)
理解SwingUtilities類作用的前提是先理解事件派發線程的概念。
當運行一個 Swing 程式時,會自動建立三個線程。
1.主線程,負責執行main 方法。
2. toolkit 線程,負責捕捉系統事件,比如鍵盤、滑鼠移動等,程式員不會有任何代碼在這個線程上執行。Toolkit線程的作用是把自己捕獲的事件傳遞給第三個線程,也就是事件派發線程。
3. 事件派發線程(EDT,Event Dispatcher Thread),顧名思義是用來派發事件(根據事件找到對應的事件處理代碼)的線程。EDT接收來自 toolkit 線程的事件,並且將這些事件組織成一個隊列,EDT的工作內容就是將這個隊列中的事件按照順序派發給相應的事件監聽器,並且呼叫事件監聽器中的回呼函數,這也意味著,所有的事件處理代碼都是在EDT而不是主線程中執行。
上面說到EDT中維護了一個事件的隊列,並且它們是按照順序派發的。由於事件派發是單線程的操作,所以只有等待前面事件監聽器的回呼函數執行完畢,才能夠動作項目更新的操作,以及繼續派發後面的事件。這樣導致的一個後果就是:當在一個事件監聽回呼函數中做了耗時的操作,那麼,介面會因此停住,並且介面上所有控制項失效(不可觸發)。
解決這個問題的方法是:在事件處理函數中將耗時的操作放到新線程(一般稱之為背景工作執行緒)中執行,而不是讓其在EDT中執行。比如下面的例子。
案例
一個視窗,有一個按鈕和一個label。點擊按鈕,系統將做模仿匯入資料的動作,匯入資料之前需要檢測資料的合法性。並且,檢測資料和匯入資料這兩個步驟都需要耗費一定的時間。
如果沒有之前說到的EDT的概念,那麼你可能會這麼做:
importBtn.addActionListener(newActionListener() { @Override publicvoid actionPerformed(ActionEvent e) { try{ lb.setText("1.檢查資料合法性..."); Thread.sleep(3000);//模仿檢測資料合法性 lb.setText("2.正在匯入資料..."); Thread.sleep(4000);//模仿匯入資料 lb.setText("3.匯入成功!"); }catch (InterruptedException e1) { e1.printStackTrace(); } } });
但是,如果運行一下的話,會發現現象是這樣:點擊按鈕,介面卡住,按鈕變得不可觸發,直到一段時間(7秒)之後介面顯示“3.匯入成功”。期間並沒有顯示“1.檢查資料合法性”和“2.正在匯入資料”。
這個現象印證了上面說的理論:當事件派發線程中正在執行的事件監聽函數執行完畢,才能進行UI組件的重新整理操作,並且派發事件隊列中的下一個。
下面是修改後的代碼,將耗時的操作放在一個新的背景工作執行緒中執行:
importBtn.addActionListener(newActionListener() { @Override public voidactionPerformed(ActionEvent e) { new Thread(new Runnable() {//開闢一個背景工作執行緒 @Override public void run() { try { lb.setText("1.檢查資料合法性..."); Thread.sleep(3000);//模仿檢測資料合法性 lb.setText("2.正在匯入資料..."); Thread.sleep(4000);//模仿匯入資料 lb.setText("3.匯入成功!"); } catch(InterruptedException e1) { e1.printStackTrace(); } } }).start(); } });
執行結果很令人滿意:點擊按鈕,顯示檢查資料合法性,三秒鐘之後顯示正在匯入資料,四秒鐘之後顯示匯入完畢。
主題
下面到了SwingUtilities的內容。在swing編程中有一個編程原則:所有的介面相關的更新,都應該在 EDT 上執行,否則會導致介面繪製出現不穩定性錯誤。這也就意味著上面代碼的lb.setText("2.正在匯入資料...");應該在EDT中,而非新的背景工作執行緒中執行。這樣就出現了一個矛盾:耗時的操作必須要在背景工作執行緒中執行,否則會出現介面重新整理不及時和卡頓的現象,而背景工作執行緒中的介面重新整理代碼又會導致介面繪製的不穩定。
SwingUtilities可以解決這個矛盾。SwingUtilities的invokeLater和invokeAndWait方法可以將一個可執行對象(Runnable)執行個體追加到EDT的可執行隊列中。那麼最後的代碼應該是這樣的:
importBtn.addActionListener(newActionListener() { @Override public voidactionPerformed(ActionEvent e) { new Thread(new Runnable() { @Override public void run() { try { SwingUtilities.invokeLater(new Runnable() { @Override public voidrun() { lb.setText("1.檢查資料合法性..."); } }); Thread.sleep(3000);//模仿檢測資料合法性 SwingUtilities.invokeLater(new Runnable() { @Override public voidrun() { lb.setText("2.正在匯入資料..."); } }); Thread.sleep(4000);//模仿匯入資料 SwingUtilities.invokeLater(newRunnable() { @Override public voidrun() { lb.setText("3.匯入成功!"); } }); } catch(InterruptedException e1) { e1.printStackTrace(); } } }).start(); } });
關於invokeLater和invokeAndWait的區別,請自行搜尋。
結論
通過上面內容可以總結以下兩個swing編程原則
所有的介面相關的更新,都應該在 EDT 上執行
而耗時的後台運行,不應該在 EDT 上執行
擴充
對於java8來說上面的代碼可以用lambda實現的更加優雅一些,如下:
importBtn.addActionListener(e -> { new Thread(() -> { try { SwingUtilities.invokeLater(() -> lb.setText("1.檢查資料合法性...")); Thread.sleep(3000);//模仿檢測資料合法性 SwingUtilities.invokeLater(()-> lb.setText("2.正在匯入資料...")); Thread.sleep(4000);//模仿匯入資料 SwingUtilities.invokeLater(() -> lb.setText("3.匯入成功!")); } catch (InterruptedExceptione1) { e1.printStackTrace(); } }).start(); });
SwingUtilities的invokeLater和invokeAndWait