用JavaFX已經有一段時間了,一直沒有時間去總結一下。今天有空把JavaFX中的多線程編程總結一下。
本文章主要參考JavaFX的官方文檔:http://docs.oracle.com/javase/8/javafx/interoperability-tutorial/concurrency.htm,代碼基於Java8。
像所有的界面編程一樣,JavaFX的界面一樣是單線程的。JavaFX中負責訪問和修改界面的線程叫做Application Thread, 它並不是線程安全的。以下例子可以證明:
package application; import javafx.application.Application; import javafx.collections.ObservableList; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; public class NoThreadSafeJavaFXUI extends Application { @Override public void start(Stage primaryStage) { try { AnchorPane root = new AnchorPane(); Label label = new Label("舊的值"); Button button = new Button(); label.setLayoutX(100); label.setLayoutY(100); label.setPrefWidth(100); button.setLayoutX(100); button.setLayoutY(150); button.setText("開始"); button.setOnAction((event) -> { Thread thread = new Thread() { @Override public void run() { label.setText("新的值"); } }; thread.setName("thread1"); thread.start(); }); ObservableList<Node> children = root.getChildren(); children.add(label); children.add(button); Scene scene = new Scene(root, 400, 400); primaryStage.setScene(scene); primaryStage.show(); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { launch(args); } }
本例子的目的是當用戶點擊按鈕的時候,文字會從“舊的值”變成“新的值”。但是如果你點擊按鈕,你會發現JavaFX程序會報一個
Not on FX application thread; currentThread = thread1的異常。導致這個異常的原因就是因為修改界面的工作必須要由JavaFX的Application Thread來完成,由其它線程來完成不是線程安全的。
解決的辦法也很簡單,JavaFX提供了一個工具方法,可以把修改界面的工作放到一個隊列中,等到Application Thread空閑的時候,它就會自動執行隊列中修改界面的工作了。以下就是把Thread中的run方法修改了一下。
@Override public void run() { Platform.runLater(() -> { label.setText("新的值"); }); }
這樣的話文字就可以成功的被修改為“新的值”了。
基於多線程去處理異步的任務或者耗時的任務是一個非常普遍的情況,雖然Java已經為我們提供了java.util.concurrent包下面相當豐富的多線程的類來幫助我們編寫多線程的任務,不過就像上面的例子一樣,會難免會使我們犯了在非Application Thread中修改界面這樣不小心的錯。為此,JavaFX為我們准備了javafx.concurrent包下面的幾個類來保證和界面交互的工作發生在正確的線程上,官方文檔也推薦我們使用javafx.concurrent包下面的類來編寫JavaFX的多線程的代碼。
打開javafx.concurrent包下面,你會發現一共就6個類:
核心的類是Worker接口,該接口抽象了一個異步工作所應該需要的屬性和方法:工作的狀態(state);工作完成應該返回的值(value);如果工作有異常的異常(exception);工作是否完成(workdone);總工作量(totalWork);進度(progress);工作是否正在進行(isRunning);異步工作處理中返回的一些消息(message),例如:正在處理3個圖片中的第1個;可選的一個異常處理Worker的標題,例如(正在處理圖片);cancel()方法,用來終止當前Worker。Worker接口的State屬性有六個狀態:READY, SCHEDULED, RUNNING, SUCCESSED, CANCELLED, FAILED。它們之前的關系如下圖。
實現Worker接口有兩個類:Task和Service,下面重點介紹這兩個類
Task:Task類是用來實現你要在后台線程處理的任務,比如從遠程讀取一個數據或者執行一個比較耗時的一個計算。來看一下Task的代碼(截取主要部分)
public abstract class Task<V> extends FutureTask<V> implements Worker<V>, EventTarget { protected abstract V call() throws Exception; /** * A protected convenience method for subclasses, called whenever the * state of the Task has transitioned to the SUCCEEDED state. * This method is invoked on the FX Application Thread after the Task has been fully transitioned to * the new state. * @since JavaFX 2.1 */ protected void succeeded() { } protected void running() { } protected void scheduled() { } protected void cancelled() { } protected void failed() { } // 還有很多實現了Worker接口中的方法 }
首先我們要實現的方法就是call方法,這是我們后台線程執行后台邏輯的地方。其次Task還提供了successed, running, scheduled, cancelled, failed等方法,我摘取了successed方法的javadoc出來,可以發現,這些方法的調用都會被JavaFX的Application Thread調用的。這樣說明了當后台程序執行完畢的時候,修改界面的工作應該放到這些方法里去執行,這樣代碼既清晰,又避免了使用Application.runLater。
Service: Service是用來在一個或者多個后台線程中執行一個Task,Service類的方法和狀態同樣只能在JavaFX的Application Thread里面。Serivce類的目的同樣是為了讓開發者可以正確的處理后台線程和Application線程之間的關系。與Task不同的是,Service可以reset和restart一個Task,所以Service類可以重用Task類,因此對於需要重復執行相同任務的工作可以使用Service類來對Task來進行重用。其它方法和Task類差不多,在此我就不多提了,有興趣的同學可以自行看源碼。另外Service還有一個子類ScheduledService,它可以實現像java.util.concurrent.ScheduledExecutorService一樣延遲,周期的執行一個Task。
Serivce和Task一樣,同樣是實現了Worker類,Servcie類同樣是一個抽象類,唯一必須要實現的抽象方法是createTask方法,返回一個后台線程執行的Task。一個Service可以通過以下幾種方式來執行:
- 通過java.util.concurrent.Executor來執行一個Service
- 或者你可以新建一個deamon線程
- 或者ThreadPoolExecutor
最后javafx.concurrent包下面還有一個WorkerStateEvent類,細心的同學會發現Task和Service類都實現了EventTarget接口,所以Service和Task在上圖中的狀態切換時,都會產生WorkerStateEvent,這樣我們就可以實現監聽器來監聽Task的狀態從而可以實現一些特定的業務。
JavaFX的多線程編程就講到這里了,Java本身已經自帶了非常豐富的多線程想着的類了,不同的是只是JavaFX為了實現特有的UI的線程安全及方便開發者正確處理后台線程和Application線程,才封裝了javafx.concurrent包下面的這幾個類。相信熟悉Java多線程編程的同學一定會很容易理解的。