1. 程式人生 > >SwingUtilities類中的invokeLater()和invokeAndWait()方法理解

SwingUtilities類中的invokeLater()和invokeAndWait()方法理解

      在理解SwingUtilities類中的.invokeLater()和invokeAndWait()兩種方法之前,需要先了解清楚Swing執行緒機制。Swing程式通常包括三種類型的執行緒:①初始化執行緒(Initial Thread);②任務執行緒(Work Thread);③事件排程執行緒(Event Dispatch Thread,EDT)。

      初始化執行緒主要負責啟動程式的UI介面,一旦UI介面啟動完成,初始化執行緒的工作就結束了,每個程式都是從main方法開始執行,該方法一般就是執行在初始化執行緒上。工作執行緒主要負責執行和介面無直接關係的耗時任務和輸入/輸出密集型操作,任何高染或延遲UI事件的處理都會由該執行緒完成。EDT主要負責繪製和更新介面,並響應使用者的輸入,每個Swing程式都會有一個EDT,通過EDT管理一個事件佇列,使用者每次對介面的更新請求(如鍵盤、滑鼠移動等事件)都會排到事件佇列中等待EDT的處理,EDT會將佇列中的事件按照順序派發給相應的事件監聽器,並且呼叫事件監聽器中的回撥函式。

      通過上面的內容可以瞭解到EDT管理了一個事件佇列,並且它們是按照順序派發的。由於事件派發是單執行緒的操作,所以只有等待前面事件監聽器的回撥函式執行完畢,才能夠執行元件更新的操作以及繼續派發後面的事件。這樣導致的一個後果就是:當在一個事件監聽回撥函式中做了耗時的操作就會導致UI介面因此停住,並且UI介面上所有的控制元件失效(不可觸發)。針對這樣的問題,我們想到的解決辦法就是將事件處理函式中的耗時操作放到新執行緒(任務執行緒)中執行,而不是讓其在EDT中執行。例如:以下程式模擬提交資料過程,點選提交按鈕(putButton)後需要進行檢查資料和提交資料兩個耗時過程。

putButton.addActionListener(newActionListener() {  
           @Override  
            public void actionPerformed(ActionEvent e) {  
                try{  
                   lb.setText("正在檢查資料");  
                   Thread.sleep(4000);//模仿檢查資料的耗時過程  
                   lb.setText("正在提交資料");  
                   Thread.sleep(1000);//模仿提交資料的耗時過程  
                   lb.setText("提交成功");  
                }catch (InterruptedException e1) {  
                   e1.printStackTrace();  
                }  
            }  
        }); 

      會看到點選提交按鈕後介面停住,5秒後(兩個耗時操作執行完成)UI介面顯示“提交成功”的提示資訊,“正在檢查資料”和“正在提交資料”兩句提示語並沒有顯示。這正是上面講的需要在事件監聽函式執行完畢後才能進行UI元件的重新整理操作並接著派發事件佇列中的下一個事件。將上面的程式碼修改如下(耗時操作放在一個新執行緒中):

putButton.addActionListener(newActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                new Thread(new Runnable() {//開闢一個工作執行緒
                    @Override
                    public void run() {
                        try {
                            lb.setText("正在檢查資料");  
                            Thread.sleep(4000);//模仿檢查資料的耗時過程  
                            lb.setText("正在提交資料");  
                            Thread.sleep(1000);//模仿提交資料的耗時過程  
                            lb.setText("提交成功");
                        }catch(InterruptedException e1) {
                            e1.printStackTrace();
                        }
                    }
                }).start();
            }
        });

      會看到UI介面依次顯示“正在檢查資料”、“正在提交資料”、“提交成功”。這正是我們期望的結果。

      但是在編寫Swing程式時,需要注意兩點:①不能從其他非EDT執行緒訪問UI元件和事件處理器,否則會使程式出現非執行緒安全問題;②不能在EDT中執行耗時任務,這會使得更新UI介面的操作阻塞在佇列中得不到處理,使程式失去響應。這樣的話就有一個矛盾:耗時操作必須在工作執行緒中執行,否則會使UI介面產生停住等現象,而工作執行緒中更新UI介面又會出現非執行緒安全問題。為了解決這個矛盾就要用到SwingUtilities類中的invokeLater()和invokeAndWait()兩種方法,通過這兩種方法可以將一個可執行物件(Runnable)例項追加到EDT的可執行事件佇列中。例如將上面的程式碼修改如下:

putButton.addActionListener(newActionListener() {  
            @Override  
            public void actionPerformed(ActionEvent e) {  
                new Thread(new Runnable() {  
                    @Override  
                    public void run() {  
                        try {  
                           SwingUtilities.invokeLater(new Runnable() {  
                                @Override  
                                public void run() {  
                                    lb.setText("正在檢查資料");  
                                }  
                            });  
                           Thread.sleep(4000);//模仿檢查資料的耗時過程  
                           SwingUtilities.invokeLater(new Runnable() {  
                                @Override  
                                public void run() {  
                                   lb.setText("正在提交資料");  
                                }  
                            });  
                           Thread.sleep(1000);//模仿提交資料的耗時過程  
                            SwingUtilities.invokeLater(newRunnable() {  
                                @Override  
                                public void run() {  
                                   lb.setText("提交成功");  
                                }  
                            });  
                        } catch(InterruptedException e1) {  
                           e1.printStackTrace();  
                        }  
                    }  
                }).start();  
            }  
        }); 

      以上程式碼中我們將更新UI介面的程式碼放到一個Runnable中,作為引數傳遞給invokeLater,由它將更新UI元件的事件放到EDT維護的一個事件佇列中,EDT會在合適的時候從佇列中取出該Runnable並執行其run方法,這樣的話仍然是EDT在更新UI介面而不會出現執行緒安全問題。

      此外,對於Java 8的話還可以用Lambda表示式來實現以上程式碼:

putButton.addActionListener(e -> {  
           new Thread(() -> {  
               try {  
                  SwingUtilities.invokeLater(() -> lb.setText("正在檢查資料"));  
                  Thread.sleep(4000);//模仿檢查資料的耗時過程  
                  SwingUtilities.invokeLater(()-> lb.setText("正在提交資料"));  
                  Thread.sleep(1000);//模仿提交資料的耗時過程  
                  SwingUtilities.invokeLater(() -> lb.setText("提交成功"));  
                } catch (InterruptedExceptione1) {  
                  e1.printStackTrace();  
                }  
           }).start();  
       });

invokeLater()和invokeAndWait()兩種方法的區別

      invokeLater()方法是非同步的,即將在EDT將事件放到事件佇列中就返回;而invokeAndWait()方法是同步的,即在EDT將事件放到事件佇列中等待其Runnable執行完畢才返回,即invokeAndWait()方法會阻塞呼叫該方法的執行緒,直到對應的run方法執行完成。因此,不能使用EDT來呼叫invokeAndWait()方法,否則容易導致前面出現的UI介面停住等現象。