JavaFX はスレッドセーフではありません。
これは JavaFX に限ったことではなく、 世の中に存在するほぼすべての UI ツールキットはシングルスレッドモデルとして設計されており、 スレッドセーフではありません。AWT、 Swing、 Android、 .NET Framework の WindowsForms、 WPF、 Linux を含むクロスプラットフォームの GTK、 いずれもスレッドセーフではないのです。(WPF は独立した描画スレッドを持っていますが、 開発者が触れるのは UI スレッドのみであり他のツールキットと同様の UI スレッド制御が必要であることからシングルスレッド UI として分類しました。)
シングルスレッドモデルの UI ツールキットでは、 UI を操作できるスレッドが 1 つに制限されています。このスレッドの呼び方は様々で、 JavaFX では 「FX アプリケーションスレッド」、 AWT や Swing では 「イベントディスパッチスレッド (EDT)」、 Android、 WindowsForms、 GTK では 「メインスレッド」 と呼ばれています。これらの UI 操作可能なスレッドを総称して 「UI スレッド」 と呼ぶこともあります。
UI にアクセスできるスレッドは 1 つに制限されているため、 UI 操作を UI スレッドで実行するための手段が UI ツールキットに用意されているのが一般的です。
Swing
Swing には SwingUtilities.invokeLater メソッドと SwingUtilities.invokeAndWait メソッドがあります。どちらもワーカースレッドから呼び出すメソッドで、 引数として渡した Runnable を UI スレッドで実行してくれます。
invokeLater
メソッドの呼び出しは Runnable の完了を待たず、 すぐに復帰します。invokeAndWait
メソッドの呼び出しは Runnable が完了するまでブロックされます。
JavaFX
JavaFX には Platform.runLater メソッドが用意されています。
runLater
メソッド呼び出しは Runnable の完了を待たず、 すぐに復帰します。
残念ながら、 JavaFX には SwingUtilities.invokeAndWait に相当するメソッドが提供されていません。これでは、 UI 操作が完了するのを待ってワーカースレッドの処理を進めたいときに困ってしまいます。
というわけで、 SwingUtilities.invokeAndWait の JavaFX 版 runAndWait
メソッドを作ってみます。難しいことはありません。CountDownLatch を使って、 非同期実行される Platform.runLater の待ち合わせをすれば、 Runnable の完了まで待機する同期実行バージョンになります。
runAndWaitpublic static void runAndWait(Runnable runnable)
throws InterruptedException, InvocationTargetException {
if(Platform.isFxApplicationThread()) {
throw new Error("Cannot call runAndWait from the FX Application Thread");
}
Throwable[] throwable = new Throwable[1];
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
try {
runnable.run();
} catch(Throwable t) {
throwable[0] = t;
} finally {
latch.countDown();
}
});
latch.await();
if(throwable[0] != null) {
throw new InvocationTargetException(throwable[0]);
}
}
これだけです。このコードを適当なユーティリティ ・ クラスに貼り付ければ使えます。
この runAndWait
メソッドはワーカースレッドから呼び出す必要があります。このメソッドを JavaFX アプリケーションスレッド (UI スレッド) から呼び出すとエラーがスローされます。
UIスレッドから呼び出した場合はエラーにするif(Platform.isFxApplicationThread()) {
throw new Error("Cannot call runAndWait from the FX Application Thread");
}
エラーを発生させずに Runnable を UI スレッドでそのまま実行するように実装することもできます。
UIスレッドから呼び出した場合はそのまま実行するif(Platform.isFxApplicationThread()) {
runnable.run();
return;
}
ですが、 私はこの実装を選択しませんでした。開発者は、 runAndWait
メソッドの呼び出し元が UI スレッドなのかワーカースレッドなのかを明確に意識すべきだからです。
「現在のスレッドが UI スレッドかどうかよく分からないけど runAndWait
を呼べば UI スレッドで実行される」
このようなルーズな使い方は、 思わぬバグに繋がる危険性が高く許容すべきではありません。開発中やデバッグ時に 「とりあえず動く」 よりも 「エラーで停止する」 ほうが、 最終的には品質の高い安全なプログラムになると信じています。
SwingUtilities.invokeAndWait メソッドも、 イベントディスパッチスレッド (EDT) から呼び出してしまった場合は "Cannot call invokeAndWait from the event dispatcher thread"
というエラーが発生するようになっています。
JavaFXのソースコードを読んでみると…
実は、 JavaFX には SwingUtilities.invokeAndWait に相当するメソッド runAndWait
が非公開 API として実装されていました。
PlatformImpl.javapublic static void runAndWait(final Runnable r) {
runAndWait(r, false);
}
private static void runAndWait(final Runnable r, boolean exiting) {
if (isFxApplicationThread()) {
try {
r.run();
} catch (Throwable t) {
System.err.println("Exception in runnable");
t.printStackTrace();
}
} else {
final CountDownLatch doneLatch = new CountDownLatch(1);
runLater(() -> {
try {
r.run();
} finally {
doneLatch.countDown();
}
}, exiting);
if (!exiting && toolkitExit.get()) {
throw new IllegalStateException("Toolkit has exited");
}
try {
doneLatch.await();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
この PlatformImpl
クラスのパッケージは com.sun.javafx.application
であり、 (特別な事情がなければ) 開発者がアクセスすべきクラスではありません。
公開 API である javafx.application.Platform
クラスのソースコードを読んでみると、 こちらにも runAndWait
の名残りが見つかります。
Platform.java// NOTE: Add the following if we decide to expose it publicly
// public static void runAndWait(Runnable runnable) {
// PlatformImpl.runAndWait(runnable);
// }
「公開することにしたら、 このコードを追加してください (コメントアウトを外してください)」 と書かれています。runAndWait
の実装はできているけど、 敢えて非公開という判断をしているようですね。
この JavaFX のソースコードに存在する runAndWait
メソッドですが、 このまま公開 API にするには実装が良くないなあ、 と私は思いました。気になったのは 2 箇所です。
1 つは、 JavaFX アプリケーションスレッド (UI スレッド) から呼び出した際に、 エラーが発生することなく、 そのまま Runnable を実行してしまう点です。Swing と同様に続行せずエラーを発生させるほうが安全なのではないかと思います。
もう 1 つは、 例外発生時にスタックトレースが標準出力に送られるだけで実質的に例外が握り潰されている点です。
runAndWait
の独自実装と、 JavaFX のソースコードに隠された runAndWait
実装の紹介は以上です。