今回はJavaFXにおけるスレッドの扱いについて確認していく。
■ JavaFXアプリケーションスレッド
JavaFXのプログラムは、特にスレッドを作成しない限りJavaFXアプリケーションスレッドという1つのスレッド上で動作する。JavaFXアプリケーションスレッドとはApplication::launch関数で起動されるスレッドであり、JavaFXアプリケーションスレッド上でstart関数は動作している。もし、別のスレッド(バッググラウンドスレッド)を作成した場合、JavaFXのシーングラフはスレッドセーフではないのでJavaFXアプリケーションスレッドからアクセスするように意識する必要がある。ちなみに、バックグラウンドスレッドからシーングラフへのアクセスが検出されるとランタイムエラーが発生してしまう。
また、JavaFXアプリケーションスレッドでは画面描写や各種イベントの取得も行っている。このためThread::sleep関数呼出や重い処理をJavaFXアプリケーションスレッド上で実行すると画面がフリーズしたり、キー入力を受け付けなくなってしまうので注意が必要である。以下にJavaFXアプリケーションスレッドの動作を確認するサンプルプログラムを示す。サンプルプログラムでは、Thread::sleep関数呼び出しによってアニメーション(画面描写)がフリーズすることを確認できる。
◇サンプルコード
package application;
import javafx.animation.Interpolator;
import javafx.animation.RotateTransition;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TestThread1 extends Application
{
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception
{
// フォント色がおかしくなることへの対処
System.setProperty( "prism.lcdtext" , "false" );
// シーングラフの作成
BorderPane root = new BorderPane();
Rectangle rect = new Rectangle( 40 , 40 );
Button button = new Button( "sleep!" );
root.setCenter( rect );
root.setBottom( button );
// シーンの作成
Scene scene = new Scene( root , 100 , 100 );
// ウィンドウ表示
primaryStage.setScene( scene );
primaryStage.show();
// アニメーションの追加
RotateTransition anim = new RotateTransition( Duration.millis(3000) , rect );
anim.setByAngle( 180 );
anim.setInterpolator( Interpolator.LINEAR );
anim.setCycleCount( RotateTransition.INDEFINITE );
anim.play();
// ボタンのイベントハンドラ
button.addEventHandler( ActionEvent.ACTION ,
e -> { try {
Thread.sleep( 1000 );
} catch (Exception e1) {
e1.printStackTrace();
}
} );
}
}
◇実行結果
◇解説
サンプルでは正方形が回り続けるアニメーションが表示されるが、「sleep!」ボタンを押下すると1秒間アニメーションがフリーズする。これはボタン押下時に起動するイベントハンドラ内でThread::sleep関数を起動し、JavaFXアプリケーションスレッドを1秒間停止しているためである(51行目)。
■ JavaFX専用のスレッドクラス
上記のように画面描写のフリーズ等を防ぐためには、バックグラウンドスレッドを利用する必要がある。JavaFXには専用のスレッドクラスがあり、状態遷移や状態遷移に伴って起動するイベントハンドラの機構が備わっている。またJavaFX専用のスレッドクラスは、エラーや取消しに関する変更通知、publicプロパティやイベント・ハンドラおよび状態の変更がすべてJavaFXアプリケーションスレッド上で実行されることが保証されている。もちろんJavaで一般的名スレッドクラス(Thread等)も利用可能ではあるが、よほどの事情がない限りはJavaFX専用スレッドクラスを使うべきということのようだ。JavaFX専用のスレッドクラスは以下の3つである。
- Taskクラス
- Serviceクラス
- ScheduledServiceクラス
それぞれのクラスについて使用方法を見ていく。
Task
再利用できない1回かぎりの処理を実行するクラス。TaskクラスはRunnableインタフェースの実装でもあり、ThreadやExcecutorクラスに渡すことによりバックグラウンドスレッドとして起動する。
//ラベル作成
Label label1 = new Label( "now loading..." );
// ラベルの文字を5秒後に変更するタスクを定義
Task<Boolean> task = new Task<Boolean>()
{
@Override
protected Boolean call() throws Exception
{
// 5秒待つ
Thread.sleep( 5000 );
// ラベルの値を変更する
// Platform.runLater関数により、
// 変更はJavaFXアプリケーションスレッド上で処理される
Platform.runLater( () -> label1.setText( "Hello world!" ) );
return true;
}
};
// タスクを実行1
Thread t = new Thread( task );
t.setDaemon( true );
t.start();
// タスクを実行2
//ExecutorService ex = Executors.newSingleThreadExecutor();
//ex.execute( task );
//ex.shutdown();
Service
再利用可能な処理を実行するクラス。内部で処理1回分のTaskクラスを定義する必要がある。executorプロパティを指定しない場合は、デーモンスレッド(プログラム終了時に消えるスレッド)として処理を実行する。
//ラベルの作成
Label label2 = new Label( "now loading..." );
// ラベルの文字を10秒後に変更するサービスを定義
Service<Boolean> service = new Service<Boolean>()
{
// 経過時間を保持
private int time = 0;
@Override
protected Task<Boolean> createTask()
{
// タスクを定義
Task<Boolean> task = new Task<Boolean>()
{
@Override
protected Boolean call() throws Exception
{
// 10秒待つ
Thread.sleep( 10000 );
// 経過時間を更新
time += 10;
// ラベルの値を変更する
// Platform.runLater関数により、
// 変更はJavaFXアプリケーションスレッド上で処理される
Platform.runLater( () -> label2.setText( "current time is " + time ) );
return true;
};
};
// 作成したタスクを返す
return task;
};
};
// サービスを開始
service.start();
ScheduledService
処理完了後に自動で再起動するServiceクラス。
// ラベルの作成
Label label3 = new Label( "now loading..." );
// ラベルの文字を15秒後と、以降5秒間隔で変更するサービスを定義
ScheduledService<Boolean> ss = new ScheduledService<Boolean>()
{
// 経過時間を保持
private int time = 10;
@Override
protected Task<Boolean> createTask()
{
// タスクを定義
Task<Boolean> task = new Task<Boolean>()
{
@Override
protected Boolean call() throws Exception
{
// 5秒待つ
Thread.sleep( 5000 );
// 経過時間を更新
time += 5;
// ラベルの値を変更する
// Platform.runLater関数により、
// 変更はJavaFXアプリケーションスレッド上で処理される
Platform.runLater( () -> label3.setText( "current time is " + time ) );
return true;
};
};
// 作成したタスクを返す
return task;
}
};
// 10秒遅れでサービスを実行
ss.setDelay( Duration.seconds( 10 ) );
ss.start();
■ 状態遷移とイベントハンドラ
JavaFX専用のスレッドクラスで利用可能な状態と、状態が変化した場合に起動するイベントハンドラについて確認していく。
状態・状態遷移
JavaFX専用のスレッドクラスが持つ状態と状態遷移は以下の通りである。
- READY(インスタンス化の直後)
- SCHEDULED(処理開始が指示され、実際に処理が行われるまでの間)
- RUNNING(処理実行中)
- SUCCEEDED(処理が成功して完了した場合)
- FAILED(処理が失敗し完了した場合)
- CANCELLED(処理を途中でキャンセルした場合)
JavaFX専用スレッドクラスの状態遷移図
処理の取消
ユーザはcancel関数を呼び出すことによって、スレッドの処理を中断させることができる。ただし、Javaではスレッドを確実に停止させる方法がないため、繰り返し文やブロッキング関数の処理中に取消処理が発生した場合にはユーザコードによる対応が必要である。
取消処理の発生タイミング |
ユーザコードによる対応 |
繰り返し処理中
(for, while等) |
isCancelled関数でキャンセル状態か確認し、処理を分岐させる |
ブロッキング関数呼び出し中
(Thread::sleep, InputStream::read等) |
InterruptedExceptionが発生するため、例外処理内でisCancelled関数を呼び出して処理を分岐させる |
イベントハンドラ
状態遷移時に起動するイベントハンドラは以下の通りである。イベントハンドラには2種類あり、1つはイベントハンドラ関数を登録する方法であり、もう1つはスレッドクラス内部でprotected指定されたメソッドをオーバーライドする方法である。イベント発生時の実行順は、イベントハンドラ→protectedメソッドの順。各状態に対応するイベントハンドラの指定方法を以下に示す。 実行例についてはサンプルプログラム参照のこと。
状態 |
イベントハンドラ登録関数 |
protectedメソッド |
READY |
- |
- |
SCHEDULED |
setOnScheduled(EventHandler<WorkerStateEvent>) |
scheduled()のオーバーライド |
RUNNING |
setOnRunninng(EventHandler<WorkerStateEvent>) |
running()のオーバーライド |
SUCCEEDED |
setOnSucceeded(EventHandler<WorkerStateEvent>) |
succeeded()のオーバーライド |
FAILED |
setOnCanceled(EventHandler<WorkerStateEvent>) |
failed()のオーバーライド |
CANCELLED |
setOnFailed(EventHandler<WorkerStateEvent>) |
canceled()のオーバーライド |
■ スレッド間の変数の引渡し
JavaFXアプリケーションスレッド - バックグラウンドスレッド間で変数を引き渡す方法としては、以下のようなものがある。1,2以外の方法は、JavaFXに限らずJavaでスレッド間の変数引渡しに利用される方法である。
- スレッドクラスの値取得・設定関数を利用する
- バックグラウンドスレッド内でPlatform.runLaterを呼び出し、JavaFXアプリケーションスレッド内の変数に値を設定する
- JavaFXアプリケーションスレッドのfinal変数(定数)をバッググラウンドスレッド上で参照する
- synchronizedやSemaphore、ReentrantReadWriteLock等の排他機構を利用して、変数の引渡しを行う
- マルチスレッドセーフな変数(AtomicInteger等)やコレクション(ConcurrentLinkedQueue等)を利用して、変数の引渡しを行う
1のスレッドクラスの値取得・設定関数の一覧は以下のとおりである。
値取得関数
(JavaFXアプリケーションスレッド側) |
値設定関数
(バックグラウンドスレッド側) |
getValue() |
updateValue( T ) |
getMessage() |
updateMessage( String ) |
getState() |
- |
getTitle() |
updateTitle( String ) |
getProgress() |
updateProgress( double , double )
updateProgress( long , long ) |
getWorkDone() |
*updateProgressの第1引数で設定 |
getTotalWork() |
*updateProgressの第2引数で設定 |
2のPlatform.runLater関数は、引数として渡されたRunnableクラスをJavaFXアプリケーションスレッド上で実行する関数である。このRunnableクラス内でJavaFXアプリケーションスレッドの変数を操作する。Runnableクラス実行のタイミングは将来の任意とされており、後続すべてのRunnableクラスよりも先に実行する仕様となっている。おそらく、できるだけ早いタイミングで実行するけどJavaFXアプリケーションスレッドがbusyな時には後回しにするよ、というニュアンスと思われる。
3~5については、JavaFXの範囲外なので別の機会に確認する。
■ サンプルプログラム
上記で確認してきたスレッドクラスの使い方(状態遷移とイベントハンドラ、スレッド間での変数引渡し)について、動作確認するサンプルプログラムを示す。サンプルでは画面に出力したラベルと進捗インジケータの値が時間とともに変化していく。また、ボタン押下により進捗インジケータの動作を中止する。
◇サンプルコード
package application;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TestThread2 extends Application
{
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception
{
// フォント色がおかしくなることへの対処
System.setProperty( "prism.lcdtext" , "false" );
// シーングラフの作成
VBox root = new VBox();
Node taskTestNode = createTaskTestNode();
Node serviceTestNode = createServiceTestNode();
Node sServiceTestNode = createSServiceTestNode();
root.getChildren().addAll( taskTestNode, serviceTestNode, sServiceTestNode );
// シーンの作成
Scene scene = new Scene( root , 300 , 100 );
// ウィンドウ表示
primaryStage.setScene( scene );
primaryStage.show();
}
/**
* Taskクラスのテスト用ノード作成
* 画面表示5秒後に値が変わるラベル
*
* @return
*/
public Node createTaskTestNode()
{
// 画面出力用のラベルを作成
Label label = new Label( "now loading..." );
// ラベルの文字を5秒後に変更するタスクを定義
Task<Boolean> task = new Task<Boolean>()
{
@Override
protected Boolean call() throws Exception
{
// 5秒待つ
Thread.sleep( 5000 );
// ラベルの値を変更する
// Platform.runLater関数により、
// 変更はJavaFXアプリケーションスレッド上で処理される
Platform.runLater( () -> label.setText( "Hello world!" ) );
return true;
}
};
// タスクを実行
ExecutorService ex = Executors.newSingleThreadExecutor();
ex.execute( task );
ex.shutdown();
return label;
}
/**
* Serviceクラスのテスト用ノード作成
* 1%づつ進捗率が上がる進捗インジケータ
* @return
*/
public Node createServiceTestNode()
{
// 画面表示用の進捗インジケータと
// 進捗中止(取消)用のボタンを作成
HBox root = new HBox();
ProgressIndicator pi = new ProgressIndicator( 0 );
Button button = new Button( "Service cancel!" );
root.getChildren().addAll( pi , button );
// インジケータの値を1%づつ上げていくサービスを作成
Service<Boolean> service = new Service<Boolean>()
{
// 定数定義
final long max = 10000; // 完了までの時間[ms]
final long inc = 100; // 進捗が1%終わる時間[ms]
@Override
protected Task<Boolean> createTask()
{
// タスクを定義
Task<Boolean> task = new Task<Boolean>()
{
@Override
protected Boolean call() throws Exception
{
// 進捗状況を0.1秒ごとに上昇させる
for( int i=0 ; i<(max/inc) ; i++ )
{
// 0.1秒待つ
try
{
Thread.sleep( inc );
}finally{
// キャンセルされていないか確認
if( isCancelled() ){ break; }
}
// 進捗を更新
this.updateProgress( (i+1) * inc , max );
}
// タスクを終了
return true;
};
};
// 作成したタスクを返す
return task;
};
};
// 進捗インジケータと進捗状況をリンクさせる
service.progressProperty().addListener( e -> pi.setProgress( service.getProgress() ) );
// ボタン押下時にサービスを中止するように設定
button.addEventHandler( ActionEvent.ACTION , e -> service.cancel() );
// サービスを開始
service.start();
return root;
}
/**
* ScheduledServiceクラスのテスト用ノード作成
* 現在のScheduledServiceクラスの状況を出力するラベル
* @return
*/
public Node createSServiceTestNode()
{
// ラベルの作成
HBox root = new HBox();
Label label1 = new Label( "now loading..." );
Label label2 = new Label( "now loading..." );
root.getChildren().addAll( label1 , label2 );
// 状況
ScheduledService<Boolean> ss = new ScheduledService<Boolean>()
{
// カウンタ
int count = 0;
@Override
protected Task<Boolean> createTask()
{
// タスクを定義
Task<Boolean> task = new Task<Boolean>()
{
@Override
protected Boolean call() throws Exception
{
// カウンタ値を表示
Platform.runLater( () -> label2.setText( "count is " + count ) );
// 5秒待つ
Thread.sleep( 5000 );
// カウンタ値をインクリメント
count++;
// タスクを終了
return true;
};
};
// タスクを返す
return task;
}
@Override
protected void running()
{
// 変更前の関数を呼び出し
super.running();
// 追加する処理を記述
Platform.runLater( () -> System.out.println( "Running! (by protected method)" ) );
}
};
// サービスの状態変更時、ラベル1のテキストを変更するように設定
ss.stateProperty().addListener( e -> label1.setText( ss.getState().toString() + ":" ) );
// Scheduled状態に遷移した際、ラベル2のテキストを変更するように設定
ss.addEventFilter(WorkerStateEvent.WORKER_STATE_SCHEDULED , e -> System.out.println( "Scheduled! (by EventHandler)" ) );
// 7秒遅れでサービスを実行
ss.setDelay( Duration.seconds( 7 ) );
ss.start();
return root;
}
}
◇実行結果
Scheduled! (by EventHandler)
Running! (by protected method)
Scheduled! (by EventHandler)
Running! (by protected method)
…
◇解説
Taskクラスの利用方法はcreateTaskTestNode関数(57行目~86行目)で確認できる。バックグラウンドスレッドとして処理を起動させるにはTask::call関数をオーバーライドして(66行目~76行目)、ExcecutorService::execute関数に渡せばよい。注目してほしいのは、JavaFXアプリケーションスレッドの変数labelに対して操作を行う際にPlatform::runLater関数を利用する記述方法である(74行目)。
Serviceクラスの利用方法はcreateServiceTestNode関数(93行目~155行目)で確認できる。バックグラウンドスレッドとして処理を起動させるにはService::createTask関数をオーバーライドして(110行目~141行目)、Service::start関数を呼び出せばよい。注目してほしいのは2点。まずは、バックグラウンドスレッドでブロッキング関数であるThread::sleepを呼ぶ際に、キャンセル状態になっていないか確認する記述方法である(127行目)。2点目は、バックグラウンドスレッドにおける進捗率の更新(131行目)とJavaFXアプリケーションスレッドにおける進捗率の取得(146行目)には、Platform::runLater関数を使わなくてもいい点である。
ScheduledServiceクラスの利用方法はcreateSServiceTestNode関数(162行目~215行目)で確認できる。バックグラウンドスレッドとして処理を起動させる方法はServiceクラスと同じである(225行目)。注目してほしいのは3点。1つめはタスクが完了すると自動的に状態が再開する点である。特に自動再生設定をする必要はない。2つめはScheduledService::Serviceクラスのオーバーライドにより、状態遷移時に処理を行っている記述方法である(206行目~213行目)。3つめはイベントハンドラで状態遷移時に処理を行っている記述方法である(221行目)。
■ 参照
- Java Platform, Standard Edition (Java SE) 8 - チュートリアル(同時実行性およびスレッドの使用)
- Java Doc 「インタフェースWorker」
- Java Doc 「クラスTask」
- Java Doc 「クラスService」
- Java Doc 「クラスScheduledService」
- ITpro 「JavaFX 2ではじめる、GUI開発 第14回 非同期処理」