JavaFXのスレッドルール
スレッドとJavaFXには2つの基本的なルールがあります。
- シーングラフの一部であるノードの状態を変更またはアクセスするコードは、必須です。 JavaFXアプリケーションスレッドで実行されます。その他の特定の操作(例:新しい
Stage
の作成 s)もこのルールに拘束されます。 - 実行に時間がかかる可能性のあるコードはすべき バックグラウンドスレッドで実行されます(つまり、FXアプリケーションスレッドでは実行されません)。
最初のルールの理由は、ほとんどのUIツールキットと同様に、フレームワークはシーングラフの要素の状態を同期せずに記述されているためです。同期を追加するとパフォーマンスコストが発生し、これはUIツールキットにとって法外なコストであることがわかります。したがって、この状態に安全にアクセスできるのは1つのスレッドだけです。 UIスレッド(JavaFXのFXアプリケーションスレッド)はシーンをレンダリングするためにこの状態にアクセスする必要があるため、FXアプリケーションスレッドは「ライブ」シーングラフ状態にアクセスできる唯一のスレッドです。 JavaFX 8以降では、このルールの対象となるほとんどのメソッドがチェックを実行し、ルールに違反した場合はランタイム例外をスローします。 (これは、「違法な」コードを記述でき、正常に実行されているように見えるSwingとは対照的ですが、実際には、任意の時点でランダムで予測できない障害が発生する傾向があります。)これが原因です。 IllegalStateException
あなたが見ている : courseCodeLbl.setText(...)
を呼び出しています FXアプリケーションスレッド以外のスレッドから。
2番目のルールの理由は、FXアプリケーションスレッドがユーザーイベントの処理を担当するだけでなく、シーンのレンダリングも担当するためです。したがって、そのスレッドで長時間実行される操作を実行すると、その操作が完了するまでUIはレンダリングされず、ユーザーイベントに応答しなくなります。これにより、例外が生成されたり、オブジェクトの状態が破損したりすることはありませんが(ルール1に違反する場合)、(せいぜい)ユーザーエクスペリエンスが低下します。
したがって、完了時にUIを更新する必要がある長時間実行操作(データベースへのアクセスなど)がある場合、基本的な計画は、バックグラウンドスレッドで長時間実行操作を実行し、操作の結果を返すことです。完了してから、UI(FXアプリケーション)スレッドでUIの更新をスケジュールします。すべてのシングルスレッドUIツールキットには、これを行うメカニズムがあります。JavaFXでは、 Platform.runLater(Runnable r)
を呼び出すことでこれを行うことができます。 r.run()
を実行します FXアプリケーションスレッドで。 (Swingでは、 SwingUtilities.invokeLater(Runnable r)
を呼び出すことができます。 r.run()
を実行します AWTイベントディスパッチスレッドで。)JavaFX(この回答の後半を参照)は、FXアプリケーションスレッドへの通信を管理するための高レベルのAPIも提供します。
マルチスレッドの一般的なグッドプラクティス
複数のスレッドを操作するためのベストプラクティスは、「ユーザー定義」スレッドで実行されるコードを、固定状態で初期化され、操作を実行するメソッドを持ち、完了するとオブジェクトを返すオブジェクトとして構造化することです。結果を表します。初期化された状態と計算結果に不変オブジェクトを使用することが非常に望ましいです。ここでの考え方は、可変状態が複数のスレッドから見える可能性を可能な限り排除することです。データベースからデータにアクセスすることは、このイディオムにうまく適合します。データベースアクセスのパラメーター(検索用語など)を使用して「ワーカー」オブジェクトを初期化できます。データベースクエリを実行して結果セットを取得し、結果セットを使用してドメインオブジェクトのコレクションにデータを入力し、最後にコレクションを返します。
場合によっては、複数のスレッド間で可変状態を共有する必要があります。これを絶対に行う必要がある場合は、その状態へのアクセスを慎重に同期して、一貫性のない状態の状態を観察しないようにする必要があります(状態の活性など、対処する必要のある他のより微妙な問題があります)。これが必要な場合の強力な推奨事項は、高レベルのライブラリを使用してこれらの複雑さを管理することです。
javafx.concurrentAPIの使用
JavaFXは、同時実行API
を提供します。 これは、バックグラウンドスレッドでコードを実行するために設計されており、APIは、そのコードの実行の完了時(または実行中)にJavaFXUIを更新するために特別に設計されています。このAPIは、 java.util.concurrent
API
、マルチスレッドコードを作成するための一般的な機能を提供します(ただし、UIフックはありません)。 javafx.concurrent
のキークラス はタスク
です。
、これは、バックグラウンドスレッドで実行することを目的とした単一の1回限りの作業単位を表します。このクラスは、単一の抽象メソッド call()
を定義します。 、パラメータを受け取らず、結果を返し、チェックされた例外をスローする場合があります。 タスク
Runnable
を実装します そのrun()
で call()
を呼び出すだけのメソッド 。 タスク
updateProgress(...)
、 updateMessage(...)
、など。いくつかの監視可能なプロパティを定義します(例:状態
および value コード>
):これらのプロパティのリスナーには、FXアプリケーションスレッドの変更が通知されます。最後に、ハンドラーを登録するための便利なメソッドがいくつかあります(
setOnSucceeded(...)
、 setOnFailed(...)
、など);これらのメソッドを介して登録されたハンドラーは、FXアプリケーションスレッドでも呼び出されます。
したがって、データベースからデータを取得するための一般的な式は次のとおりです。
タスク
を作成します データベースへの呼び出しを処理します。タスク
を初期化します データベース呼び出しを実行するために必要な任意の状態で。- タスクの
call()
を実装します データベース呼び出しを実行し、呼び出しの結果を返すメソッド。 - タスクにハンドラーを登録して、完了時に結果をUIに送信します。
- バックグラウンドスレッドでタスクを呼び出します。
データベースアクセスの場合、UIについて何も知らない別のクラスに実際のデータベースコードをカプセル化することを強くお勧めします(データアクセスオブジェクトのデザインパターン )。次に、タスクにデータアクセスオブジェクトのメソッドを呼び出させます。
したがって、次のようなDAOクラスがある可能性があります(ここにはUIコードがないことに注意してください):
public class WidgetDAO {
// In real life, you might want a connection pool here, though for
// desktop applications a single connection often suffices:
private Connection conn ;
public WidgetDAO() throws Exception {
conn = ... ; // initialize connection (or connection pool...)
}
public List<Widget> getWidgetsByType(String type) throws SQLException {
try (PreparedStatement pstmt = conn.prepareStatement("select * from widget where type = ?")) {
pstmt.setString(1, type);
ResultSet rs = pstmt.executeQuery();
List<Widget> widgets = new ArrayList<>();
while (rs.next()) {
Widget widget = new Widget();
widget.setName(rs.getString("name"));
widget.setNumberOfBigRedButtons(rs.getString("btnCount"));
// ...
widgets.add(widget);
}
return widgets ;
}
}
// ...
public void shutdown() throws Exception {
conn.close();
}
}
一連のウィジェットの取得には長い時間がかかる可能性があるため、UIクラス(コントローラークラスなど)からの呼び出しはすべて、バックグラウンドスレッドでこれをスケジュールする必要があります。コントローラクラスは次のようになります:
public class MyController {
private WidgetDAO widgetAccessor ;
// java.util.concurrent.Executor typically provides a pool of threads...
private Executor exec ;
@FXML
private TextField widgetTypeSearchField ;
@FXML
private TableView<Widget> widgetTable ;
public void initialize() throws Exception {
widgetAccessor = new WidgetDAO();
// create executor that uses daemon threads:
exec = Executors.newCachedThreadPool(runnable -> {
Thread t = new Thread(runnable);
t.setDaemon(true);
return t ;
});
}
// handle search button:
@FXML
public void searchWidgets() {
final String searchString = widgetTypeSearchField.getText();
Task<List<Widget>> widgetSearchTask = new Task<List<Widget>>() {
@Override
public List<Widget> call() throws Exception {
return widgetAccessor.getWidgetsByType(searchString);
}
};
widgetSearchTask.setOnFailed(e -> {
widgetSearchTask.getException().printStackTrace();
// inform user of error...
});
widgetSearchTask.setOnSucceeded(e ->
// Task.getValue() gives the value returned from call()...
widgetTable.getItems().setAll(widgetSearchTask.getValue()));
// run the task using a thread from the thread pool:
exec.execute(widgetSearchTask);
}
// ...
}
(潜在的に)実行時間の長いDAOメソッドの呼び出しが Task
にどのようにラップされているかに注目してください。 これは、UIのブロックを防ぐために(アクセサーを介して)バックグラウンドスレッドで実行されます(上記のルール2)。 UIの更新( widgetTable.setItems(...)
)は、 Task
を使用して、FXアプリケーションスレッドで実際に実行されます。 の便利なコールバックメソッド setOnSucceeded(...)
(ルール1を満たす)。
あなたの場合、実行しているデータベースアクセスは単一の結果を返すので、
のようなメソッドがあるかもしれません。public class MyDAO {
private Connection conn ;
// constructor etc...
public Course getCourseByCode(int code) throws SQLException {
try (PreparedStatement pstmt = conn.prepareStatement("select * from course where c_code = ?")) {
pstmt.setInt(1, code);
ResultSet results = pstmt.executeQuery();
if (results.next()) {
Course course = new Course();
course.setName(results.getString("c_name"));
// etc...
return course ;
} else {
// maybe throw an exception if you want to insist course with given code exists
// or consider using Optional<Course>...
return null ;
}
}
}
// ...
}
そして、コントローラーコードは次のようになります
final int courseCode = Integer.valueOf(courseId.getText());
Task<Course> courseTask = new Task<Course>() {
@Override
public Course call() throws Exception {
return myDAO.getCourseByCode(courseCode);
}
};
courseTask.setOnSucceeded(e -> {
Course course = courseTask.getCourse();
if (course != null) {
courseCodeLbl.setText(course.getName());
}
});
exec.execute(courseTask);
タスク
>
progress
の更新など、さらに多くの例があります タスクのプロパティ(プログレスバーなどに役立ちます。