Java Swing執行緒之SwingUtilities.invokeLater解釋

ImportNew發表於2015-04-21

譯者注:本文其實是一個各方見解評論的總結,已將其中於此處釋出不妥的內容去掉,全文內容可檢視原文。

在官方的文件裡:http://docs.oracle.com/javase/tutorial/uiswing/painting/step1.html 告訴我們如何建立一個gui。

事件分發執行緒:

Swing中事件處理和繪畫程式碼都在一個單獨的執行緒中執行,這個執行緒就叫做事件分發執行緒。這就確保了事件處理器都能序列的執行,並且繪畫過程不會被事件打斷。為了避免死鎖的可能,你必須極度小心從事件分發執行緒中建立、修改、查詢Swing元件以及模型。

注意:我們過去常說只要你沒有修改已經實現過的元件,你就能在主程式中建立GUI。[補充:下面頁注中的紅色字型。] 已實現過的意思是元件已經在螢幕上描繪出來或是準備描繪了。方法setVisible(true)和pack可以實現一個視窗,反過來又可以實現該視窗內 包含的元件。儘管這對大多數應用程式都管用,但這種做法在某些情況下會引起一些問題。在Swing Tutorial的所有示例中,我們只在ComponentEventDemo中遇到一個問題。在那個樣例中,有時候當你載入樣例後,它並不會啟動。因為 如果在文字域還沒實現的時候就去更新會出現死鎖,但是其他的時候沒有意外的話它也是會正常啟動。

為了避免執行緒問題,建議你使用invokeLater在事件分發執行緒中為所有新應用程式建立GUI。如果你的現有程式能工作正常,那你可能就會讓它保持下去;然而,如果改造起來方便的話,還是希望你能改造一下。

你可能已經注意 到,大部分教程中的例子都使用一個標準的主函式,即SwingUtilities的函式invokeLater來保證GUI在事件分發執行緒中建立。這裡有 一個從FocusConceptsDemo例子中提取的主函式的樣例。我們還將處理建立GUI事件的主函式都要呼叫的一個私有靜態方法,即 createAndShowGUI 的原始碼包含進來了。

/**
 * Create the GUI and show it.  For thread safety,
 * this method should be invoked from the
 * event-dispatching thread.
 */
private static void createAndShowGUI() {
    //Make sure we have nice window decorations.
    JFrame.setDefaultLookAndFeelDecorated(true);

    //Create and set up the window.
    frame = new JFrame("FocusConceptsDemo");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    //Create and set up the content pane.
    JComponent newContentPane = new FocusConceptsDemo();
    newContentPane.setOpaque(true); //content panes must be opaque
    frame.setContentPane(newContentPane);

    //Display the window.
    frame.pack();
    frame.setVisible(true);
}

public static void main(String[] args) {
    //Schedule a job for the event-dispatching thread:
    //creating and showing this application's GUI.
    javax.swing.SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            createAndShowGUI();
        }
    });
}

使用invokeLater方法

你可以從任何執行緒中呼叫invokeLater來請求事件分發執行緒以執行某段代 碼。你必須將這段程式碼放入一個Runnable物件的run方法中,並將該指定Runnable物件作為引數傳遞給invokeLater。 invokeLater函式會立即返回,不會等到事件分發執行緒執行完這段程式碼。這裡有一個使用invokeLater的例子:

Runnable updateAComponent = new Runnable() {
    public void run() { component.doSomething(); }
};
SwingUtilities.invokeLater(updateAComponent);

invokeLater必須放在run()方法體內。

使用invokeAndWait方法

invokeAndWait方法和 invokeLater方法一樣,除了invokeAndWait是直到事件分發執行緒已經執行了指定程式碼才返回。任何可能的時候,你都應當使用 invokeLater而不是invokeAndWait——因為invokeAndWait很容易引起死鎖。如果你使用invokeAndWait,要 保證呼叫invokeAndWait的執行緒不持有任何其他執行緒在呼叫時刻也會需要的鎖。

這裡有一個使用invokeAndWait的例子:

void showHelloThereDialog() throws Exception {
    Runnable showModalDialog = new Runnable() {
        public void run() {
            JOptionPane.showMessageDialog(myMainFrame,
                                          "Hello There");
        }
    };
    SwingUtilities.invokeAndWait(showModalDialog);
}

類似地,一個需要獲取GUI狀態的執行緒也需要類似的處理,比如包含兩個文字域的元件,可能會有如下程式碼:

void printTextField() throws Exception {
    final String[] myStrings = new String[2];

    Runnable getTextFieldText = new Runnable() {
        public void run() {
            myStrings[0] = textField0.getText();
            myStrings[1] = textField1.getText();
        }
    };
    SwingUtilities.invokeAndWait(getTextFieldText);

    System.out.println(myStrings[0] + " " + myStrings[1]);
}

使用執行緒提高效能

使用恰當的話,執行緒會是 一個很有用的工具。然而,當在一個Swing程式中使用執行緒時,你必須謹慎處理。雖然有危險,但執行緒還是很有用的。你可以使用執行緒提高你程式的響應效能。 而且,執行緒有時還能簡化程式的程式碼或結構。這裡有一些使用執行緒的特殊場景:

  • 將一個比較消耗時間的初始化任務移出主執行緒,可以使GUI出現得更快。耗時任務包括做額外的計算任務以及網路阻塞或硬碟I/O(比如,載入圖片)。
  • 將耗時任務移出事件分發執行緒,從而使GUI同時繼續響應使用者操作。
  • 為了能重複執行一個操作,通常還需要在操作之間設定一個預置時間段(定時器)。
  • 等待其他程式的訊息。

如果你需要建立一個執行緒,可以通過使用工具類SwingWorker或是Timer類中的一個實現該執行緒,這樣一來能夠避免一些常見的陷阱。SwingWorker物件建立一個執行緒以執行一個耗時操作。

當該操作結束後,SwingWoker會在事件分發執行緒中給你提供執行額外程式碼的選項。Timer類則適合重複執行或需要一段延時執行的操作。如果你需要實現自己的執行緒,可以在Concurrency中找到相關資訊。

可以使用一些技巧來使多執行緒的Swing程式有更好的效能:

  • 如果當你需要更新一個元件但事件監聽器中的程式碼並沒有執行的時候,可以考慮使用這兩個方法,SwingUtilities的invokeLater(優先選項)或invokeAndWait方法。
  • 如果你不能確定事件監聽器中的程式碼 是否已經執行,那你就應當分析程式程式碼和執行緒中每個函式的呼叫檔案。如果還是不行,可以使用SwingUtilities的 isEventDispatchThread方法。當該方法在事件分發執行緒中執行時返回true。你能在任何執行緒中安全地呼叫invokeLater,但 invokeAndWait會在呼叫執行緒不是事件分發執行緒時丟擲異常。
  • 如果你需要在一段延遲之後更新元件(不論你的程式碼目前是否正在一個事件監聽器中執行),那就使用定時器timer吧。
  • 如果你需要在沒經過一段規律的時間間隔後更新元件,使用定時器timer。

使用timer定時器的資訊和例子,請見如何使用Swing定時器

使用SwingWorker類

注意:SwingWorker類的實現已經有過兩次更新了,最近的一次是2000年的2月。第一次更新(在1999年1月)允許程式安全地中斷工作執行緒。最近的一次更新(稱作“SwingWorker 3”)修正了一個會引起空指標異常NullPointerException的隱藏執行緒bug。

類SwingWorker在SwingWorker.java中 實現,它沒有在Swing包中釋出。使用SwingWorker類之前,需要先建立一個SwingWorker的子類。子類必須要實現建構函式使它能包含 執行耗時操作的程式碼。當初始化SwingWorker子類的時候,SwingWorker類建立了一個執行緒但沒啟動它(截至SwingWorker 3)。然後才是呼叫SwingWorker物件的啟動start方法來啟動執行緒,就是呼叫建構函式。

這裡有一個例子,使用SwingWorker類將耗時任務從動作事件監聽器中移動到後臺執行緒中,從而使GUI能保持響應。

//OLD CODE:
public void actionPerformed(ActionEvent e) {
    ...
    //...code that might take a while to execute is here...
    ...
}

//BETTER CODE:
public void actionPerformed(ActionEvent e) {
    ...
    final SwingWorker worker = new SwingWorker() {
        public Object construct() {
            //...code that might take a while to execute is here...
            return someValue;
        }
    };
    worker.start();  //required for SwingWorker 3
    ...
}

建構函式的返回值可以是任意的物件。想獲取該返回值,可以呼叫SwingWorker物件的get函式。對於get函式要小心使用。因為它會阻塞,會引起死鎖。如果有必要的話,可以呼叫SwingWorker的中斷函式interrupt中斷執行緒(引起函式返回)。

如果你想在耗時 任務完成的時候更新GUI,可以呼叫get函式(這個正如需要注意的那樣,有些危險)或是重寫你SwingWorker子類的finished函式。 Finished函式會在建構函式返回後執行。因為finished函式在事件分發執行緒中執行,你能安全地使用它更新Swing元件。當然,你最好不要把 耗時操作放進finished函式中。

下面實現finished函式的例子是從IconDemoApplet.java檔案中拿來的。若想充分地討論這個applet,包括如何使用後臺執行緒載入圖片以提高響應效能,請看如何使用Icons

public void actionPerformed(ActionEvent e) {
    ...
    if (icon == null) {     //haven't viewed this photo before
        loadImage(imagedir + pic.filename, current);
    } else {
        updatePhotograph(current, pic);
    }
}
...
//Load an image in a separate thread.
private void loadImage(final String imagePath, final int index) {
    final SwingWorker worker = new SwingWorker() {
        ImageIcon icon = null;

        public Object construct() {
            icon = new ImageIcon(getURL(imagePath));
            return icon; //return value not used by this program
        }

        //Runs on the event-dispatching thread.
        public void finished() {
            Photo pic = (Photo)pictures.elementAt(index);
            pic.setIcon(icon);
            if (index == current)
                updatePhotograph(index, pic);
        }
    };
    worker.start(); 
}

更多使用SwingWorker的例子,請看How to Monitor Progress。還有,TumbleItem.java,在How to Make Applets中討論過的,既使用了SwingWorker,還使用了計時器Timer。

討論:http://www.ime.uerj.br/javatutor/uiswing/misc/threads.html

官方解釋:

public static void invokeLater(Runnable doRun)

函式public static void invokeLater(Runnable doRun)引起函式doRun.run()在AWT的事件分發執行緒中非同步執行。這個函式應該在程式執行緒需要更新GUI的時候呼叫。下面的例子中,invokeLater呼叫事件分發執行緒的Runnable物件doHelloWorld到佇列中,然後列印一條資訊。

 Runnable doHelloWorld = new Runnable() {
     public void run() {
         System.out.println("Hello World on " + Thread.currentThread());
     }
 };

如果是從事件分發執行緒呼叫的invokeLater——比如,從一個JButton的監聽器中——doRun.run()會一直延遲到事件佇列中的所有待處理事件都處理完才 執行。要注意,當doRun.run()丟擲一個未捕獲的異常時,事件分發執行緒會釋放掉(不是當前執行緒)。

更多關於這個函式的檔案和例子請參見Java 教程中的How to Use Threads

至於1.3部分,這個函式是由java.awt.EventQueue.invokeLater()包裝而來。

不像Swing的其他函式,這個函式可以從任意執行緒中呼叫。

還可參見:

相關文章