使用Swing Worker線程 --執行後臺任務的新方法

使用Swing Worker線程 --執行後臺任務的新方法
FooSleeper 翻譯  (參與分:71054,專家分:1637)   發表:2003-07-11 17:01   更新:2003-07-13 23:21   版本:1.0   閱讀:7403

作者:Hans Muller,Kathy Walrath 
翻譯:郭曉剛([email protected]
原文來自java.sun.com

本文給出了一些使用SwingWorker類的例子。SwingWorker類的目的是實現一個後臺線程,讓你可以用它來執行一些費時的操作,而不影響你的程序的GUI的性能。關於SwingWorker類的一些基本信息,請參閱《線程和Swing》。
注意:在2000年9月我們修改了這篇文章和它的例子以適用於一個更新版本的SwingWorker類。SwingWorker類的這個版本修正了一些微妙的線程bug。
對執行一些費時或可阻塞操作的Swing程序來說,線程是基本的解決之道。例如,如果你的應用程序要根據用戶選擇的菜單項發出一個數據庫請求或加載一個文件,那麼你應該在一個單獨的線程中完成這些工作。本文闡述了在一個分離的worker線程中完成上述工作的途徑。
本文包括以下主要內容:
  • SwingWorker類:這一部分告訴你怎樣下載SwingWorker類並描述了SwingWorker類的用途。介紹了SwingWorker類的interrupt()方法。
  • 引入Worker線程的例子:演示一個運用SwingWorker類的應用程序。
  • 例一:中斷一個Swing Worker線程:解釋如何運用interrupt()方法來中斷worker線程。
  • 例二:從Swing Worker線程反饋給用戶:例一的增強,添加了一個模式對話框以提示用戶輸入。


概覽:SwingWorker類


因爲SwingWorker類並不是Java發行版的一部分,你需要下載和編譯它才能使用。它的源代碼在這裏:
SwingWorker.java
SwingWorker類簡介

SwingWorker類可以簡單且方便地用於在一個新的線程中計算一個數值。要使用這個類,你只要創建一個SwingWorker的子類並覆蓋SwingWorker.construct()方法來執行計算。然後你實例化它,並在這個新實例上調用start()方法。
例如,下面的代碼片斷中產生了一個線程,其中構造了一個字符串。然後,片斷中使用了get()方法來取得前面由construct方法所返回的值,並且在必要時將等待。最後,在顯示器上顯示出字符串的值。
  1.  
  2. SwingWorker worker = new SwingWorker() {
  3.    public Object construct() {
  4.       return "Hello" + " " + "World";
  5.    }
  6. };
  7. worker.start();
  8. System.out.println(worker.get().toString());

在實際的應用程序中,construct方法會做些更有用(但可能很費時)的事情。比如,它可能做下列工作之一:
  • 執行大量的運算
  • 執行可能導致大量的類被裝載的代碼
  • 爲網絡或磁盤I/O阻塞
  • 等待其他資源

在上面的代碼片斷中沒有展示的一個SwingWorker類的特性是,當construct()返回後,SwingWorker可以讓你在事件派發線程中執行一些代碼。你可以通過覆蓋SwingWorker的finished()方法來做到這一點。典型地,你可以用finished()來顯示剛剛構造的一些組件或設置組件上顯示的數據。
原始版本的SwingWorker類的一個侷限是,一旦一個worker線程開始運行,你無法中斷它。(譯註:本文、本文的例子及SwingWorker類曾被更新。)不管怎麼說,對於一個交互應用程序來說,讓用戶在工作線程完成前一直等待是相當糟糕的風格。如果用戶希望終止一項正在執行中的操作,執行此操作的線程應該能夠儘快中止。
使用interrupt()方法

在第二版的SwingWorker類中加入了一個interrupt()方法以允許中斷一個線程。你的線程應該以下面兩種途徑之一得到中斷的通知:
  • 正在執行諸如sleep()或wait()方法的線程在interrupt()被調用時會拋出一個InterruptedException。
  • 線程可以顯式詢問它是否已被中斷,通過形如以下代碼:
    1. Thread.interrupted()
     

使用sleep()或wait()方法的工作線程(如後面例子中的線程)一般不需要顯式檢查是否被中斷。通常讓sleep()或wait()方法拋出InterruptedException就足夠了。
不過,如果你希望能夠中斷一個不含定時循環的SwingWorker,還是需要用interrupted()方法來顯式檢查中斷。

引入Worker線程的例子


本文餘下的部分討論一個包含兩個worker線程的例子的程序。下圖是程序主窗口的截圖,和它彈出的一個對話框:


例子的源代碼由以下文件組成:

你可以下載一個包含上面所有文件的zip文件。在此zip文件中還包含一個帶有HTML格式的說明的例子,這個版本的例子更能說明問題(但也更復雜)。
運行此例子前要先編譯:
/usr/local/java/jdk1.3.0/bin/javac ThreadsExample.java
用以下命令行運行此例子:
/usr/local/java/jdk1.3.0/bin/java ThreadsExample
當你按下“Start”按鈕,相應例子的worker線程將被創建。你可以在進度條中查看它的進度。你可以按下“Cancel”按鈕來中斷worker線程。在啓動例子二稍等幾秒,你會看到一個對話框提示你確認是否執行。稍後我們再詳述這個。

例子一:中斷Swing Worker線程


接着我們要討論的例子是Example1.java。這個例子中的worker線程包含一個執行100次的循環,並在兩次循環之間睡眠半秒。
  1. //progressBar maximum is NUMLOOPS (100) progressBar的最大值是NUMLOOPS (100)
  2. for(int i = 0; i < NUMLOOPS; i++) {
  3.    updateStatus(i);
  4.    ...
  5.    Thread.sleep(500);
  6. }

爲了向你展示如何使用interrupt()方法,我們的例子程序可以讓你啓動一個SwingWorker然後等待它完成或者中斷它。程序中的SwingWorker子類的construct()方法所作的唯一一件事就是調用Example1的doWork()方法。doWork()方法的完整源碼列在下面。這個例子在處理worker線程時會重置進度條並把標籤設爲“Interrupted”。因爲中斷可能發生在sleep()方法調用之外,所以代碼中在調用sleep()方法之前要先檢查中斷與否。
  1.  
  2. Object doWork() {
  3.    try {
  4.       for(int i = 0; i < NUMLOOPS; i++) {
  5.          updateStatus(i);
  6.          if (Thread.interrupted()) {
  7.             throw new InterruptedException();
  8.          }
  9.          Thread.sleep(500);
  10.       }
  11.    }
  12.    catch (InterruptedException e) {
  13.       updateStatus(0);
  14.       return "Interrupted";  
  15.    }
  16.    return "All Done"
  17. }

在此方法中執行的費時操作應該:週期性地讓用戶知道它有所進展。updateStatus()方法會爲事件派發線程排隊Runnable對象(記住:不要在其他線程中執行GUI工作)。一旦按下“Start”按鈕,動作監聽器(action listener)會創建SwingWorker,使得worker線程被創建。Worker線程啓動後,它將執行它的construct()方法,該方法將調用doWork()(如下面的代碼所示)。下面的代碼例示了Example1實現的SwingWorker的子類。
  1.  
  2. worker = new SwingWorker() {
  3.    public Object construct() {
  4.       return doWork();
  5.    }
  6.    public void finished() {
  7.       startButton.setEnabled(true);
  8.       interruptButton.setEnabled(false);
  9.       statusField.setText(get().toString());
  10.    }
  11. };

finished()方法在construct()方法返回後執行(即worker線程完成後)。它的任務只是簡單地重新使“Start”按鈕有效,同時使“Cancel”按鈕無效,並將狀態域顯示的值設置成worker的計算結果。記住finished()方法是在事件派發線程中執行的,所以它可以安全地直接更新GUI。

例子二:從Worker線程提示用戶


這個例子實現爲Example1的子類。唯一的區別是,在worker線程執行了大約兩秒後,它將阻塞直到用戶響應一個Continue/Cancel模態對話框。如果用戶選擇的不是“Continue”,我們就退出doWork()循環。
這個例子演示了一個在許多worker線程中應用的慣用法:如果worker執行中到達一個不期望的狀態,它將阻塞起來直到用戶被提醒或用一個模態對話框從用戶那裏收集到了更多信息。這種做法有一點複雜,因爲對話框的顯示需要放到事件派發線程中,且worker線程需要被阻塞直到用戶解除了該模式對話框。
我們使用SwingUtilities的invokeAndWait()方法來在事件派發線程中彈出對話框。與invokeLater()不同,invokeAndWait()會阻塞起來直到它的Runnable對象返回。在我們的例子中,Runnable對象直到對話框被解除才返回。我們創建一個內部Runnable類DoShowDialog,來完成彈出對話框。一個實例變量DoShowDialog.proceedConfirmed,被用來記錄用戶的選擇:
  1.  
  2. class DoShowDialog implements Runnable {
  3.    boolean proceedConfirmed;
  4.    public void run() {
  5.       Object[] options = {"Continue""Cancel"};
  6.          int n = JOptionPane.showOptionDialog
  7.          (Example2.this,
  8.          "Example2: Continue?",
  9.          "Example2",
  10.          JOptionPane.YES_NO_OPTION,
  11.          OptionPane.QUESTION_MESSAGE,
  12.             null,
  13.             options,
  14.             "Continue");
  15.          proceedConfirmed =
  16.             (n == JOptionPane.YES_OPTION);
  17.    }
  18. }

因爲showConfirmDialog()方法彈出一個模態對話框,調用會阻塞直到用戶解除該對話框。
爲了顯示對話框並阻塞調用線程(worker線程)直到對話框被解除,worker線程調用invokeAndWait()方法,定義一個DoShowDialog的實例:
  1.  
  2. DoShowDialog doShowDialog = new DoShowDialog();
  3. try {
  4.    SwingUtilities.invokeAndWait(doShowDialog);
  5. }
  6. catch 
  7.    (java.lang.reflect.
  8.       InvocationTargetException e) {
  9.       e.printStackTrace();
  10. }

代碼中捕獲的InvocationTargetException是調試DoShowDialog的run()方法的殘留。當invokeAndWait()方法返回後,worker線程可以讀取doShowDialog.proceedConfirmed來獲得用戶的響應。

我們要感謝Doug Lea(《Concurrent Programming in Java》的作者)、Joseph Bowbeer和其他給我們的線程文章和SwingWorker類做出回饋的讀者。
-- Hans Muller and Kathy Walrath
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章