1 JavaFXでの同時実行
この章では、javafx.concurrentパッケージによって提供される、マルチスレッド・アプリケーションを作成するための機能について説明します。
時間のかかるタスクの実行をバックグラウンド・スレッドに委任することによって、JavaFXアプリケーションのユーザー・インタフェース(UI)をレスポンス可能な状態に維持する方法について学習します。
javafx.concurrentパッケージを使用する理由
JavaFXアプリケーションのグラフィカル・ユーザー・インタフェースを表すJavaFX Sceneグラフは、スレッド・セーフではなく、JavaFXアプリケーション・スレッドとしても知られるUIスレッドからのみアクセスおよび変更できます。 時間のかかるタスクをJavaFXアプリケーション・スレッド上で実装すると、アプリケーションUIは必然的にレスポンス不可能な状態になります。 このような場合、これらのタスクを1つ以上バックグラウンド・スレッド上で実行し、JavaFXアプリケーション・スレッドでユーザー・イベントを処理できるようにすることがベスト・プラクティスです。
特殊な要件があるか、コードをより強力に制御することが必要な場合、Runnableオブジェクトと新しいスレッドを作成することによってバックグラウンド・ワーカーを実装する方法が適切です。 特定の時点でJavaFXアプリケーション・スレッドと通信して、バックグラウンド・タスクの結果または進行状況を通知する必要があることに注意してください。
ほとんどの場合、多くの開発者にとって推奨される方法は、javafx.concurrentパッケージによって提供されるJavaFX APIを使用することです。このパッケージは、UIと対話するマルチスレッド・コードを処理し、この対話が正しいスレッド上で発生することを保証します。
javafx.concurrentパッケージの概要
Javaプラットフォームでは同時実行ライブラリ一式が提供され、これはjava.util.concurrentパッケージを通じて利用可能になります。 javafx.concurrentパッケージは、JavaFXアプリケーション・スレッドや、GUI開発者が直面するその他の制約を考慮することによって、既存のAPIを活用します。
javafx.concurrentパッケージは、Workerインタフェースと、2つの具体的な実装であるTaskおよびServiceクラスで構成されます。 Workerインタフェースでは、バックグラウンド・ワーカーがUIと通信するために役立つAPIが提供されます。 Taskクラスは、java.util.concurrent.FutureTaskクラスの完全に監視可能な実装です。 Taskクラスを使用すると、開発者はJavaFXアプリケーション内に非同期タスクを実装できるようになります。 Serviceクラスはタスクを実行します。
WorkerStateEventクラスは、Worker実装の状態が変化するたびに発生するイベントを指定します。 TaskクラスとServiceクラスの両方にEventTargetインタフェースが実装されているため、状態イベントのリスニングがサポートされます。
Workerインタフェース
Workerインタフェースは、1つ以上のバックグラウンド・スレッド上でなんらかの処理を実行するオブジェクトを定義します。 Workerオブジェクトの状態は監視可能であり、JavaFXアプリケーション・スレッドから使用できます。
Workerオブジェクトのライフサイクルの定義は次のとおりです。 作成時のWorkerオブジェクトはREADY状態です。 処理がスケジュールされると、WorkerオブジェクトはSCHEDULED状態に遷移します。 その後、処理の実行時に、Workerオブジェクトの状態はRUNNINGになります。 Workerオブジェクトは、スケジュールされることなく即座に開始された場合でも、まずSCHEDULED状態に遷移し、その後RUNNING状態に遷移することに注意してください。 正常に完了した場合、Workerオブジェクトの状態はSUCCEEDEDになり、valueプロパティがこのWorkerオブジェクトの結果に設定されます。 そうではなく、実行中に例外がスローされた場合、Workerオブジェクトの状態はFAILEDになり、exceptionプロパティが、発生した例外の種類に設定されます。 開発者は、Workerオブジェクトが終了する前の任意の時点で、cancelメソッドを呼び出すことによって、Workerオブジェクトに割り込むことができます。この場合、WorkerオブジェクトはCANCELLED状態になります。
ScheduledServiceオブジェクトのライフサイクルにおける区別については、「ScheduledServiceクラス」を参照してください。
Workerオブジェクトによって実行される処理の進行状況は、totalWork、workDone、progressなど、3つの異なるプロパティを通じて取得できます。
パラメータ値の範囲の詳細は、APIドキュメントを参照してください。
Taskクラス
Taskは、バックグラウンド・スレッド上で実行する必要がある処理のロジックを実装するために使用します。 まず、Taskクラスを拡張する必要があります。 バックグラウンド処理を実行し、その結果を返すには、Taskクラスの実装でcallメソッドをオーバーライドする必要があります。
callメソッドはバックグラウンド・スレッド上で呼び出されるため、このメソッドは、バックグラウンド・スレッドからの安全な読取りおよび書込みが可能な状態のみを操作できます。 たとえば、アクティブなシーン・グラフをcallメソッドから操作すると、ランタイム例外がスローされます。 一方、TaskクラスはJavaFX GUIアプリケーションで使用するように設計されているため、publicプロパティ、エラーや取消しに関する変更通知、イベント・ハンドラおよび状態の変更はすべて、JavaFX Applicationスレッド上で発生することが保証されます。 callメソッド内では、updateProgress、updateMessageおよびupdateTitleメソッドを使用して、JavaFXアプリケーション・スレッド上の対応するプロパティの値を更新できます。 ただし、タスクが取り消された場合、callメソッドからの戻り値は無視されます。
Taskクラスは、Runnableインタフェースを実装するjava.utils.concurrent.FutureTaskクラスから継承されているため、Java同時実行ライブラリに含まれます。 このため、TaskオブジェクトはJava同時実行Executor API内で使用でき、パラメータとしてスレッドに渡すこともできます。 Taskオブジェクトは、このタスクを別のスレッドからコールできるFutureTask.run()メソッドを使用することによって、直接コールできます。 Java同時実行APIについて適切に理解すると、JavaFXにおける同時実行も理解しやすくなります。
タスクは次のいずれかの方法で開始できます。
-
特定のタスクをパラメータとして指定したスレッドの開始
Thread th = new Thread(task);th.setDaemon(true);th.start(); -
ExecutorServiceAPIの使用ExecutorService.submit(task);
Taskクラスは、再利用できない1回かぎりのオブジェクトを定義します。 再利用可能なWorkerオブジェクトが必要な場合は、Serviceクラスを使用します。
タスクの取消し
Javaでは、プロセス内のスレッドを確実に停止する方法はありません。 ただし、タスクに対してcancelがコールされるたびに、そのタスクは処理を停止する必要があります。 callメソッドの本文内でisCancelledメソッドを使用することによって、その処理中に、タスクが取り消されたかどうかを定期的に確認する必要があります。 例1-1は、取消しが発生したかどうかを確認するTaskクラスの正しい実装を示しています。
例1-1
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がスローされます。 このようなタスクに対しては、スレッドへの割込みから、タスクが取り消されたかどうかを判断できる場合があります。 このため、ブロッキング呼出しを含むタスクでは、例1-2に示すように、isCancelledメソッドを再度確認して、タスクが取り消された場合にInterruptedExceptionがスローされるようにする必要があります。
例1-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);
//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つに、バックグラウンド・タスクの進行状況の表示があります。 たとえば、1から100万までカウントするバックグラウンド・タスクと進行状況バーがあり、この進行状況バー上の進行状況を、バックグラウンドで動作するカウンタにあわせて更新する必要があるとします。 例1-3に、進行状況バーの更新方法を示します。
例1-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メソッドをオーバーライドすることによってタスクを作成し、そのタスクのprogress、totalWorkおよびworkDoneプロパティを更新するupdateProgressメソッドを呼び出します。 このことは、progressPropertyメソッドを使用して、タスクの進行状況を取得し、バーの進行状況をタスクの進行状況にバインドできるようになったため、重要です。
Serviceクラス
Serviceクラスは、1つまたは複数のバックグラウンド・スレッド上でTaskオブジェクトを実行するように設計されています。 Serviceクラスのメソッドと状態には、JavaFXアプリケーション・スレッド上でのみアクセスできます。 このクラスの目的は、開発者がバックグラウンド・スレッドとJavaFXアプリケーション・スレッド間の正しい対話を実装できるようにすることです。
Serviceオブジェクトに対して可能な制御は、必要に応じた開始、取消しおよび再起動です。 Serviceオブジェクトを開始するには、Service.start()メソッドを使用します。
Serviceクラスを使用すると、バックグラウンド処理の状態を監視し、必要に応じて処理を取り消すことができます。 後からサービスをリセットして再起動することもできます。 このため、サービスは宣言的に定義し、必要に応じて再起動できます。
自動的に再起動する必要があるサービスの詳細は、「ScheduledServiceクラス」を参照してください。
Serviceクラスのサブクラスを実装するときは、入力パラメータをサブクラスのプロパティとしてTaskオブジェクトに公開するようにします。
サービスは次のいずれかの方法で実行できます。
-
Executorオブジェクト(特定のサービスに対して指定されている場合)
-
デーモン・スレッド(エグゼキュータが指定されていない場合)
-
ThreadPoolExecutorなどのカスタム・エグゼキュータ
例1-4に、1行目を任意のURLから読み取り、それを文字列として返すServiceクラスの実装を示します。
例1-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;
}
@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イベント・ハンドラがコールされた後、protected簡易メソッドsucceededがJavaFXアプリケーション・スレッド上で呼び出されます。
protected簡易メソッドには、cancelled、failed、running、scheduled、succeededなど様々なものがあり、これらはWorker実装が対応する状態に遷移したときに呼び出されます。 これらのメソッドは、アプリケーションのロジックを実装するために状態が変更されたときに、TaskおよびServiceクラスのサブクラスによってオーバーライドできます。 例1-5に、タスクの成功、取消しおよび失敗に基づきステータス・メッセージを更新するTask実装を示します。
例1-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!");
}
};
ScheduledServiceクラス
ポーリングを実行する多くのユースケースでは、自動的に再起動するサービスが必要になります。 このような要件を満たすために、Serviceクラスを拡張したScheduledServiceクラスが作成されました。 ScheduledServiceクラスは、実行の成功後および特殊な条件下での失敗時に自動的に再起動するサービスを表します。
作成時のScheduledServiceオブジェクトはREADY状態です。
ScheduledService.start()またはScheduledService.restart()メソッドをコールした後、ScheduledServiceオブジェクトは、delayプロパティによって指定された期間、SCHEDULED状態に遷移します。
RUNNING状態のときに、ScheduledServiceオブジェクトはタスクを実行します。
タスクが正常に完了した場合
タスクの完了後、ScheduledServiceオブジェクトは、SUCCEEDED状態に遷移した後、READY状態に遷移し、その後再度SCHEDULED状態に遷移します。 SCHEDULED状態の期間は、RUNNING状態への最後の遷移が発生した時刻、現在時刻、および発生する2つの実行間の最短時間を定義するperiodプロパティの値によって異なります。 前の実行が期間内に完了した場合、ScheduledServiceオブジェクトは、その期間が終了するまでSCHEDULED状態のままになります。 そうではなく、前の実行に、指定した期間よりも長い時間がかかった場合、ScheduledServiceオブジェクトはすぐにRUNNING状態に遷移します。
タスクが失敗した場合
タスクがFAILED状態で終了した場合、ScheduledServiceオブジェクトは、restartOnFailure、backoffStrategyおよびmaximumFailureCountプロパティの値に応じて再起動または終了します。
restartOnFailureプロパティがfalseの場合、ScheduledServiceオブジェクトはFAILED状態に遷移して終了します。 この場合、失敗したScheduledServiceオブジェクトは手動で再起動できます。
restartOnFailureプロパティがtrueの場合、ScheduledServiceオブジェクトはSCHEDULED状態に遷移し、backoffStrategyプロパティをコールした結果として取得されるcumulativePeriodプロパティの期間、この状態のままになります。 cumulativePeriodプロパティを使用すると、失敗したScheduledServiceオブジェクトを、強制的に次の実行までの待機時間を長くすることができます。 ScheduledServiceが正常に完了した後、cumulativePeriodプロパティは、periodプロパティの値にリセットされます。 発生した失敗の回数がmaximumFailureCountプロパティの値に達した場合、ScheduledServiceオブジェクトはFAILED状態に遷移して終了します。
ScheduledServiceオブジェクトの実行中に発生したdelayおよびperiodプロパティの変更はすべて、次の反復で考慮されます。 delayおよびperiodプロパティのデフォルト値は0に設定されます。
まとめ
この章では、javafx.concurrentパッケージによって提供される基本的な機能について学習し、TaskおよびServiceクラスの実装に関するいくつかの例について理解しました。 Task実装の正しい作成方法に関するその他の例は、APIドキュメントのTaskクラスを参照してください。

