Règles de thread pour JavaFX
Il existe deux règles de base pour les threads et JavaFX :
- Tout code qui modifie ou accède à l'état d'un nœud faisant partie d'un graphe scénique doit être exécuté sur le thread d'application JavaFX. Certaines autres opérations (par exemple, la création d'une nouvelle
Stage
s) sont également liés par cette règle. - Tout code qui peut prendre beaucoup de temps à s'exécuter devrait être exécuté sur un thread d'arrière-plan (c'est-à-dire pas sur le thread d'application FX).
La raison de la première règle est que, comme la plupart des boîtes à outils d'interface utilisateur, le framework est écrit sans aucune synchronisation sur l'état des éléments du graphe scénique. L'ajout de la synchronisation entraîne un coût de performances, ce qui s'avère être un coût prohibitif pour les kits d'outils d'interface utilisateur. Ainsi, un seul thread peut accéder en toute sécurité à cet état. Étant donné que le thread d'interface utilisateur (FX Application Thread pour JavaFX) doit accéder à cet état pour rendre la scène, le FX Application Thread est le seul thread sur lequel vous pouvez accéder à l'état "live" du graphe de scène. Dans JavaFX 8 et versions ultérieures, la plupart des méthodes soumises à cette règle effectuent des vérifications et lèvent des exceptions d'exécution si la règle est violée. (Cela contraste avec Swing, où vous pouvez écrire du code "illégal" et il peut sembler fonctionner correctement, mais est en fait sujet à des échecs aléatoires et imprévisibles à un moment arbitraire.) C'est la cause de l'IllegalStateException
vous voyez :vous appelez courseCodeLbl.setText(...)
à partir d'un thread autre que le thread d'application FX.
La raison de la deuxième règle est que le thread d'application FX, en plus d'être responsable du traitement des événements utilisateur, est également responsable du rendu de la scène. Ainsi, si vous effectuez une opération de longue durée sur ce thread, l'interface utilisateur ne sera pas rendue tant que cette opération n'est pas terminée et ne répondra plus aux événements de l'utilisateur. Bien que cela ne génère pas d'exceptions ou ne cause pas d'état d'objet corrompu (comme le ferait la violation de la règle 1), cela crée (au mieux) une mauvaise expérience utilisateur.
Ainsi, si vous avez une opération de longue durée (telle que l'accès à une base de données) qui doit mettre à jour l'interface utilisateur à la fin, le plan de base consiste à effectuer l'opération de longue durée dans un thread d'arrière-plan, renvoyant les résultats de l'opération lorsqu'elle est terminée. terminé, puis planifiez une mise à jour de l'interface utilisateur sur le fil d'exécution de l'interface utilisateur (application FX). Tous les kits d'outils d'interface utilisateur à thread unique ont un mécanisme pour le faire :dans JavaFX, vous pouvez le faire en appelant Platform.runLater(Runnable r)
pour exécuter r.run()
sur le fil d'application FX. (Dans Swing, vous pouvez appeler SwingUtilities.invokeLater(Runnable r)
pour exécuter r.run()
sur le fil de répartition des événements AWT.) JavaFX (voir plus loin dans cette réponse) fournit également une API de niveau supérieur pour gérer la communication vers le fil d'application FX.
Bonnes pratiques générales pour le multithreading
La meilleure pratique pour travailler avec plusieurs threads consiste à structurer le code qui doit être exécuté sur un thread "défini par l'utilisateur" comme un objet qui est initialisé avec un état fixe, a une méthode pour effectuer l'opération et à la fin renvoie un objet représentant le résultat. L'utilisation d'objets immuables pour l'état initialisé et le résultat du calcul est hautement souhaitable. L'idée ici est d'éliminer autant que possible la possibilité qu'un état mutable soit visible à partir de plusieurs threads. L'accès aux données d'une base de données correspond bien à cet idiome :vous pouvez initialiser votre objet "travailleur" avec les paramètres d'accès à la base de données (termes de recherche, etc.). Exécutez la requête de base de données et obtenez un ensemble de résultats, utilisez l'ensemble de résultats pour remplir une collection d'objets de domaine et renvoyez la collection à la fin.
Dans certains cas, il sera nécessaire de partager un état mutable entre plusieurs threads. Lorsque cela doit absolument être fait, vous devez synchroniser soigneusement l'accès à cet état pour éviter d'observer l'état dans un état incohérent (il existe d'autres problèmes plus subtils qui doivent être résolus, tels que la vivacité de l'état, etc.). La forte recommandation lorsque cela est nécessaire est d'utiliser une bibliothèque de haut niveau pour gérer ces complexités pour vous.
Utilisation de l'API javafx.concurrent
JavaFX fournit une API de concurrence
qui est conçu pour exécuter du code dans un thread d'arrière-plan, avec une API spécialement conçue pour mettre à jour l'interface utilisateur JavaFX à la fin (ou pendant) l'exécution de ce code. Cette API est conçue pour interagir avec java.util.concurrent
API
, qui fournit des fonctionnalités générales pour l'écriture de code multithread (mais sans crochets d'interface utilisateur). La classe de clé dans javafx.concurrent
est Task
, qui représente une unité de travail unique et unique destinée à être effectuée sur un thread d'arrière-plan. Cette classe définit une seule méthode abstraite, call()
, qui ne prend aucun paramètre, renvoie un résultat et peut lever des exceptions vérifiées. Task
implémente Runnable
avec son run()
méthode appelant simplement call()
. Task
a également une collection de méthodes qui garantissent la mise à jour de l'état sur le thread d'application FX, telles que updateProgress(...)
, updateMessage(...)
, etc. Il définit certaines propriétés observables (par exemple state
et value
) :les auditeurs de ces propriétés seront informés des modifications apportées au fil d'application FX. Enfin, il existe des méthodes pratiques pour enregistrer les gestionnaires (setOnSucceeded(...)
, setOnFailed(...)
, etc); tous les gestionnaires enregistrés via ces méthodes seront également invoqués sur le fil d'application FX.
Donc la formule générale pour récupérer des données d'une base de données est :
- Créer une
Task
pour gérer l'appel à la base de données. - Initialiser la
Task
avec n'importe quel état nécessaire pour effectuer l'appel de la base de données. - Mettre en œuvre le
call()
de la tâche méthode pour effectuer l'appel de la base de données, renvoyant les résultats de l'appel. - Inscrivez un gestionnaire avec la tâche pour envoyer les résultats à l'interface utilisateur lorsqu'elle est terminée.
- Appelez la tâche sur un fil d'arrière-plan.
Pour l'accès à la base de données, je recommande fortement d'encapsuler le code de base de données réel dans une classe distincte qui ne sait rien de l'interface utilisateur ( Modèle de conception d'objet d'accès aux données ). Demandez ensuite à la tâche d'invoquer les méthodes sur l'objet d'accès aux données.
Vous pourriez donc avoir une classe DAO comme celle-ci (notez qu'il n'y a pas de code d'interface utilisateur ici) :
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();
}
}
La récupération d'un tas de widgets peut prendre beaucoup de temps, donc tout appel d'une classe d'interface utilisateur (par exemple, une classe de contrôleur) doit planifier cela sur un thread d'arrière-plan. Une classe de contrôleur pourrait ressembler à ceci :
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);
}
// ...
}
Remarquez comment l'appel à la méthode DAO (potentiellement) de longue durée est enveloppé dans une Task
qui est exécuté sur un thread d'arrière-plan (via l'accesseur) pour empêcher le blocage de l'interface utilisateur (règle 2 ci-dessus). La mise à jour de l'UI (widgetTable.setItems(...)
) est en fait exécuté sur le thread d'application FX, en utilisant la Task
Méthode de rappel pratique setOnSucceeded(...)
(règle 1 satisfaite).
Dans votre cas, l'accès à la base de données que vous effectuez renvoie un seul résultat, vous pouvez donc avoir une méthode comme
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 ;
}
}
}
// ...
}
Et puis votre code de contrôleur ressemblerait à
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);
La documentation API pour Task
avoir beaucoup plus d'exemples, y compris la mise à jour du progress
propriété de la tâche (utile pour les barres de progression..., etc.