用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多线程编程的同学一定会很容易理解的。