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();
-
ExecutorService
APIの使用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
クラスを参照してください。