JavaFX的同步机制
(李佳明 译自http://docs.oracle.com/javafx/2/threads/jfxpub-threads.htm 原文作者:
Irina Fedortsova Technical Writer, Oracle
)
本文描述了由javafx.concurrent包提供,用于创建多线程程序的一些功能。将会学习到如何将长时任务委派到后台执行以保证用户界面能及时响应。
为什么要使用javafx.concurrent包
代表应用程序用户界面的JavaFX场景图不是线程安全的,并且只能通过已知的JavaFX 应用程序的UI线程来访问和修改。在JavaFX应用程序线程实现一个长时任务不可避免地导至UI不响应(假死)。最好的做法是将这些长时任务放在后台的一个或多个线程中执行,而让JavaFX应用程序线程处理用户事件。
如果通过创建一个Runnable对象和新线程(Java的同步机制)的方式实现一个后台任务,而在某些情况下又必须和JavaFX应用程序线程就线程的执行结果或执行进度进行通讯,将很容易导致错误。相反,如果使用java.concurrent包中提供的JavaFX API,则能处理好与UI进行交互的多线程代码,并且保证这种交互只发生在正确的线程里面。
Javafx.concurrent包总览
Java平台在java.util.concerrent包提供了一套完整的同步类库。Javafx.concerrrent包考虑了JavaFX应用程序线程和GUI开发人员面对的各种限制,在已有的API间作了平衡。
Javafx.concerrent包由Worker接口和Task、Service两个基础类构成,这两个基础类也实现了Worker接口。Worker接口提供了后台线程与UI进行通讯的非常有用的API,Task类是java.util.concerrent.FutureTask类遵守可观察模式的实现。Task类使得开发者能够在JavaFX应用程序中实现匿名任务,而Service类则执行任务。
WorkerStateEvent类随时确认因Work对象的状态改变而触发的事件,Task类和Service类均实现了EventTarget接口,因此支持监听状态改变事件。
Worker接口
Worker接口定义在一个或多个后台线程中执行任务的对象,Worker对象的状态是可观察的,可在JavaFX应用程序线程进行访问。
Worker对象的生命周期是这样定义的:创建后,Worker对象处于READY状态;当被安排执行是,转为SCHEDULED状态;然后当Worker对象执行任务时,状态变为RUNNING。注意即使Worker对象未经安排马上执行,它也首先转为SCHEDULED状态,然后是RUNNING状态。当Worker对象的任务顺利完成,状态为SUCCEEDED,value属性的值设置为这个Worker对象的结果。反之,如果这个Worker对象在执行过程中抛出异常,它的状态为FAILED,并且exception属性的值被设置为抛出的异常类型。在Worker对象的任务未执行完之前的任何时候,开发者都可以通过调可cance方法中断任务,这将使得Worker对象的状态转变为CANCELLED。
Worker对象执行任务的进度可以通过totalWork、workDone、progress三个不同的属性获得。关于更多参数取值范围的信息,请参照API文档。
Task类
Task类用于实现一个需要在后台线程中完成的业务逻辑。首先,需要继承Task类,Task类的实现必须重写call方法,以完成后台工作并返回执行结果。
Call方法是在后台线程中调用的,因此这个方法只能在后台线程中以安全的方式读取或写入状态。例如,从call方法中操纵一个活动的场景图将导致运行时异常。另一方面,Task类是设计用于JavaFX GUI应用程序的,它可保证任何公共属性的改变、错误或取消的通知、事件处理、状态等正确地在JavaFX应用程序线程产生。在call方法内部,可以使用updateProgress、updateMessage、updateTitle方法,它们负责更新JavaFX应用程序线程中相应的属性值。但是,如果task线程被取消,从call方法返回的值将被忽略。
注意Task类适配Java同步类库,因为它继承了java.utils.concurrent.FutureTask类,该类实现了Runnable接口。正是由于这个原因,一个Task对象可以于用Java同步中的Executor API,也可以将它作为参数传第到Java线程,这将允话另一个背景线程调用这个Task。对Java同步API较好的理解可以帮助理解JavaFX的同步机制。
Task可以用以下方法启动:
- 将Task作为线程参数启动:
Threadth=new Thread(task);
th.setDeamon(true);
th.start();
- 使用ExecutorServiceAPI
ExecutorService.submit(task);
Task类定义为一次性对象,无法重用,如果需要可重用的Worker对象,可使用Service类。
取消Task
在Java中并没有提供一种可靠的方法在进程中终止线程。然而,在Task上调用cancel时任务必须终止。因此必须在call方法内使用IsCancelled方法来定期检测任务是否被取消。例1展示了Task类检测取消状态正确的实现方法。
例1:
import javafx.concurrent.Task; Task<Integer> task = newTask<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例外能根据任务的取消状态适时抛出。如例2所示:
例2:
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); //短时锁定线程,但要保证对取消进行InterruptedException 检查 try { Thread.sleep(100); } catch (InterruptedException interrupted) { if (isCancelled()) { updateMessage("Cancelled"); break; } } } return iterations; } };后台Task的完成进度
多线程程序中一个典型的用例是获取后台任务的完成情况。假设有一个后台任务对1到100000进行计数,并且要在进度条中对后台任务的完成情况进行更新。例3展示了如何更新一个进度条。
例3:
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对象,使用Service.start()方法。
使用Service类,可以观察后台工作的状态并取消它。然后,还可以重置并重启,服务可以声明方式定义并根据需要重启。
实现Service类的子类时,要保证向Task对象暴露作为子类属性的输入参数。
服务可以以下方式执行:
- 通过Executor对象,如果它被指定作为给定服务的执行器。
- 如果没有指定执行器,通过守护线程启动。
- 通过自定义的执行器,比如ThreadPoolExecutor。
例4展示了一个Service类的实现,它读取并返回一个URL的第一行。
例4:
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; } protected Task<String> createTask() { final String _url = getUrl(); return new Task<String>() { protected String call() throws IOException, MalformedURLException { String result = null; BufferedReader in = null; try { URL u = new URL(_url); in = new BufferedReader( new InputStreamReader(u.openStream())); result = in.readLine(); } finally { if (in != null) { in.close(); } } return result; } }; } } }WorkerStateEvent类和状态转换
无论何时Worker实现的状态改变,定义于WorkerStateEvent类的相应事件将会发生。例如,当Task对象的状态转变为SUCCEEDED状态,那么WORKER_STATE_SUCCEEDED事件发生,同时onSucceeded事件处理程序会被调用。然后,受保护的便利方法succeeded在JavaFX应用程序线程被调用。
有几个受保护的便利方法,比如cancelled、failed、running、scheduled和succeeded,当Worker实现的状态转变为相应状态时都会被调用。这几个方法可以在Task类或Service类的子类中重写,以便状态改变时在应用程序中实现相应的业务逻辑。
例5展示了一个Task实现,它在任务成功、取消或失败时更新状态信息。
例5:
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!"); } };总结
在本文中,我们学习了javafx.cuncurrent包提供的功能,并通过Task和Service类的几个实现熟悉了一些功能。要详细了解如何正确地创建Task实现,请参阅Task类的相关文档。