DOC-13-01 JavaFX并发
本章描述使用javafx.concurrent包提供的功能来创建多线程的应用程序。
你将学会如何在后台委托耗时的任务执行,来保持你的JavaFX应用用户界面(UI)是可响应的。
为什么要使用javafx.concurrent包?
代表了JavaFX应用程序图形用户界面的场景图并不是线程安全的,而且仅可以通过UI线程亦被称为JavaFX应用程序线程访问和修改。在JavaFX应用程序线程中执行长时间运行的任务,不可避免会造成应用程序的UI无响应。最好的实现是在一个或多个后台线程上处理这些任务,并且让JavaFX应用程序线程来处理用户事件。
如果你有特别的需求或需要额外的能力,通过创建一个Runnable对象和新的线程来实现一个后台工作是一个比较合适的方法。注意在某些情况下你必须与JavaFX应用程序进程保持通信,可能是后台任务执行的结果或进展。
对于大多数情况以及对大多数开发者来说,都推荐使用javafx.concurrent包提供的JavaFX API,它可以处理好多线程代码和UI的交互,并且可以保证这个交互发生在正确的线程上。
概览javafx.concurrent包
Java平台通过java.util.concurrent包提供了一个完整可用的并发库。javafx.concurrent包考虑到JavaFX应用程序线程和其他面向GUI开发者的限制条件,促使了现有API改变。
javafx.concurrent包由Worker接口和两个具体的实现Task和Service类组成。Worker接口提供了对后台工作与UI之间通信有用的API。Task类是java.util.concurrent.FutureTask类的一个完整的可观察的实现。Task类使开发者可以在JavaFX应用程序中实现异步任务。Service类执行这些任务。
WorkerStateEvent类指定了一个工作实现状态发生变化时的事件。Task和Service类都实现了EventTarget接口,因此可以支持监听这个状态事件。
Worker接口
Worker接口定义了一个执行一个或多个后台线程工作的对象,Worker对象的状态在JavaFX应用程序线程中是可观察且可用的。
Worker对象的生命周期定义如下。创建时,Worker对象是READY状态。开始计划工作时,Worker对象转变为SCHEDULED状态。之后当Worker对象执行工作时,状态变为RUNNING。注意当Worker对象没有计划而直接开始时,状态也是先转变为SCHEDULED再转为RUNNING状态,并且value属性也设置到Worker对象的结果中。如果任何异常在Worker对象执行过程中抛出,状态变为FAILED并且exception属性也会设置为产生的异常类型。在Worker对象结束前的任何时候,开发者都可以调用cancel方法来终止,此时状态将变为CANCELLED。
ScheduledService对象的生命周期的一些区别可以在ScheduledService类这一节中找到。
Worker对象的工作完成过程可以通过三种不同的属性获取,totalWork,workDone和progress。
可以在API文档中了解更多参数值范围的信息。
Task类
任务是用来实现需要在后台完成的工作的逻辑。首先你需要扩展Task类。实现Task的类必须重写call方法来处理后台工作和返回结果。
call方法在后台进程中被调用,因此这个方法只能操作后台线程读写安全的状态。例如,通过call方法操作一个活动的场景图会抛出运行时异常。另一方面,Task类就是设计用来和JavaFX GUI应用程序一起使用的,它能确保公有属性、错误或取消的变更通知、事件处理器和状态的修改在JavaFX应用程序线程执行。在call方法内部,你可以使用updateProgress,updateMessage,updateTitle方法,这些方法可以在JavaFX应用程序线程更新对应属性的值。但如果任务被取消时,call方法中的返回值会被忽略掉。
注意Task类兼容Java并发库是因为它继承java.utils.concurrent.FutureTask类,此类实现了Runnable接口。因此Task对象可以在Java并发Executor API中使用,也可以作为参数传递给线程。你可以使用FutureTask.run()方法来直接调用Task对象,这样可以在另一个后台线程执行该任务。如果比较了解Java并发API,可以帮助你了解JavaFX的并发。
一个任务可以由以下几种方法开始:
● 通过启动一个以指定任务作为参数的线程
1 2 3 |
Thread th = new Thread(task); th.setDaemon(true); th.start(); |
● 通过使用ExecutorService API
1 |
ExecutorService.submit(task); |
Task类定义了一个一次性且不能重用的对象。如果你需要一个可以重用的Worker对象,使用Service类。
取消任务
Java中没有可靠的方法来停止一个在运行中的线程。但是当在任务中调用cancel方法时,任务必须停止运行。任务假设在工作中周期性检查是否已被取消,可以在call方法中使用isCancelled方法检查。例1-1展示了一个正确的Task类的实现来检查取消。
例1-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import javafx.concurrent.Task; Task<Integer> task = new Task<Integer>() { @Override protected Integer call() throws Exception { int iterations; for (iterations = 0; iterations < 100000; iterations++) { if (isCancelled()) { break; } System.out.println("Iteration " + iterations); } return iterations; } }; |
如果任务实现有阻塞调用,如Thread.sleep,并且在阻塞调用中任务被取消,将会抛出一个InterruptedException。对于这些任务来说,一个中断线程可能是一个取消的任务的信号。因此任务有阻塞调用时需要双重检测isCancelled方法来确保InterruptedException是因为任务取消而抛出,如例1-2。
例1-2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import javafx.concurrent.Task; Task<Integer> task = new Task<Integer>() { @Override protected Integer call() throws Exception { int iterations; for (iterations = 0; iterations < 1000; iterations++) { if (isCancelled()) { updateMessage("Cancelled"); break; } updateMessage("Iteration " + iterations); updateProgress(iterations, 1000); //Block the thread for a short time, but be sure //to check the InterruptedException for cancellation try { Thread.sleep(100); } catch (InterruptedException interrupted) { if (isCancelled()) { updateMessage("Cancelled"); break; } } } return iterations; } }; |
展示后台任务的进度
多线程应用的典型用例是展示后台任务的进度。假设你有一个从一数到一百万的后台任务和一个进度条,你必须随着后台进度而更新进度条的进度。例1-3展示了如何更新一个进度条。
例1-3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import javafx.concurrent.Task; Task task = new Task<Void>() { @Override public Void call() { static final int max = 1000000; for (int i=1; i<=max; i++) { if (isCancelled()) { break; } updateProgress(i, max); } return null; } }; ProgressBar bar = new ProgressBar(); bar.progressProperty().bind(task.progressProperty()); new Thread(task).start(); |
首先在实现工作逻辑的代码中重写call方法创建任务并调用updateProgress方法,这将需要更新任务的progress,totalWork和workDone属性。这很重要,因为你现在可以使用ProgressProperty方法来获得任务的进度,并将任务的进度绑定到任务条上。
Service类
Service类是设计来在一个或多个后台线程上执行一个Task对象。Service类的方法和状态必须只可以由JavaFX应用程序线程访问。此类的目的是为了帮助开发者实现后台线程与JavaFX应用程序线程之间的正确交互。
对于Service对象有如下控制:你可以按需启动,取消和重启服务。使用Service.start()方法来启动Service对象。
使用Service类,你可以观察后台工作的状态也可以选择取消它。其次你可以重置服务并重启它。因此服务可以声明式定义,也可以按需要重启。
查看ScheduledService类这一节,可以了解一个需要自动重启的服务。
当实现Service类的子类时,确保向Task对象暴露作为子类属性的输入参数。
服务可以按以下其中一种执行:
● 可以被一个特定服务指定的Executor对象执行。
● 如果没有执行器被指定,则由守护进程执行。
● 由自定义的执行器执行,如ThreadPoolExecutor。
例1-4展示了一个Service类的实现,其中读取了任何一个URL的第一行并将其作为字符串返回。
例1-4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; import javafx.application.Application; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.concurrent.Service; import javafx.concurrent.Task; import javafx.concurrent.WorkerStateEvent; import javafx.event.EventHandler; import javafx.stage.Stage; public class FirstLineServiceApp extends Application { @Override public void start(Stage stage) throws Exception { FirstLineService service = new FirstLineService(); service.setUrl("http://google.com"); service.setOnSucceeded(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent t) { System.out.println("done:" + t.getSource().getValue()); } }); service.start(); } public static void main(String[] args) { launch(); } private static class FirstLineService extends Service<String> { private StringProperty url = new SimpleStringProperty(); public final void setUrl(String value) { url.set(value); } public final String getUrl() { return url.get(); } public final StringProperty urlProperty() { return url; } @Override protected Task<String> createTask() { return new Task<String>() { @Override protected String call() throws IOException, MalformedURLException { try ( BufferedReader in = new BufferedReader( new InputStreamReader( new URL(getUrl()).openStream; in = new BufferedReader( new InputStreamReader(u.openStream()))) { return in.readLine(); } } }; } } |
WorkerStateEvent类和状态转换
每当Worker实现的状态改变时,由WorkerStateEvent类定义的一个适当的事件将会发生。例如当Task对象转变为SUCCEEDED状态时,WORKER_STATE_SUCCEEDED事件将会发生,onSucceeded事件控制器将被调用,之后受保护的便利的succeed方法将被在JavaFX应用程序线程调用。
受保护的便利的方法有几种,如cancelled,failed,running,scheduled和succeeded,当Worker实现转换到对应状态时将会被调用。当状态改变时这些方法可以被Task和Service类的子类重写,来实现应用程序中的逻辑。例1-5中展示了一个Task实现,其中在任务成功,取消和失败的情况下更新了状态信息。
例1-5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import javafx.concurrent.Task; Task<Integer> task = new Task<Integer>() { @Override protected Integer call() throws Exception { int iterations = 0; for (iterations = 0; iterations < 100000; iterations++) { if (isCancelled()) { break; } System.out.println("Iteration " + iterations); } return iterations; } @Override protected void succeeded() { super.succeeded(); updateMessage("Done!"); } @Override protected void cancelled() { super.cancelled(); updateMessage("Cancelled!"); } @Override protected void failed() { super.failed(); updateMessage("Failed!"); } }; |
ScheduledService类
涉及到调用轮询的许多情况下都需要一个自动重启的服务。为了满足需求,Service类被扩展出了ScheduledService类。ScheduledService类代表了一个在成功执行后,或者在特定情况下失败后,可以自动重启的服务。
当ScheduledService对象被创建时,即为READY状态。
在调用ScheduledService.start()或者ScheduledService.restart()方法后,ScheduledService对象在delay属性定义的范围内变为SCHEDULED状态。
在处于RUNNING状态时,ScheduledService执行其任务。
任务成功完成
在任务完成后,ScheduledService对象转变为SUCCEEDED状态,然后变为READY状态,然后再变为SCHEDULED状态。再次变为SCHEDULED状态的周期取决于最后一次转变为RUNNING状态的时间、现在的时间、和定义相邻两次运行时间的最小值的period属性值。如果前一次运行结束早于周期失效,ScheduledService对象会保持在SCHEDULED状态,直到周期失效。否则如果前一次执行比指定的周期更长,ScheduledService对象会立刻转变为RUNNING状态。
任务失败
当任务以FAIL状态结束的情况下,ScheduledService对象会重启或者退出,这取决于restartOnFailure,backoffStrategy和maximumFailureCount属性的值。
如果restartOnFailure属性为false,ScheduledService对象会转变为FAILED状态并退出。在这种情况下,你可以手动重启这个已失败的ScheduledService对象。
如果restartOnFailure属性是true,ScheduledService对象会转变为SCHEDULED状态,并在cumulativePeriod属性定义的周期内保持此状态,cumulativePeriod属性可以通过调用backoffStrategy属性获取到结果。使用cumulativePeriod属性,你可以强制已失败的ScheduledService在下一次运行前等待更长。在ScheduledService成功完成后,cumulativePeriod属性会被重置为period属性的值。当失败数量达到maximumFailureCount属性的值时,ScheduledService对象会转变为FAIL状态然后退出。
在ScheduledService对象运行时,delay和period属性发生任何变化都会在下一次循环中生效。delay和period属性的默认值为0。
结论
在这一章中,你学到了javafx.concurrent包提供的基本功能,并且熟悉了Task和Service类实现的相关样例。可以查看Task类的API文档,来学习如何正确创建Task实现的更多样例。


from: FX周报(2015-09-07) 里程碑:JavaFX官方文档已经全部翻译完毕 | JavaFX中文资料 – 中文文档、入门教程
[…] 第一部分 JavaFX并发 […]
1.创建一个表格
2.表格中每一行 都是一个继承Task的对象
3.删除表格中的一行,但是内存中的对象实例并不会减少
4.在没有继承Task的时候,删除表格中的一行,内存中的实例减少了
5.继承Task,是为了实现进度条