Comment utiliser un thread d’arrière plan en Java

Évites de “geler” le GUI et frustrer les utilisateurs

Joel Leblanc
6 min readApr 2, 2021

De nos jours, c’est très rare qu’on va écrire un logiciel qui roule sur un seul thread. Souvent, l’application a une interface utilisateur graphique (GUI) et elle communique avec un API sur le web. Si on traite les interactions de l’utilisateur, les données et l’affichage sur le même thread, le logiciel aura souvent l’air “gelé”. Ça c’est vraiment plate… et c’est pour cette raison que les threads existent.

Prenons l’exemple suivant.

Matante Manon veut consulter sa boite de courriels. Elle trouve qu’Outlook est beaucoup trop compliqué, alors elle demande à son neveux/sa nièce préféré(e) informaticien(ne) si c’est possible de juste peser sur un bouton et voir ses courriels. Vu que Manon est bin fine, tu décides de lui patenter de quoi.

Les exigences sont claires. La cliente veut voir ses courriels en pesant sur un bouton. Donc, on veut un bouton et une liste.

Voici un croquis de notre client courriel :

Un croquis d’une fenêtre qui contient un tableau de trois colonnes, soient une pour l’expéditeur, une pour le sujet et une pour le message, suivi d’un bouton qui permet de télécharger les courriels.
Croquis du client courriel

Maintenant, regardons comment on peut le coder.

Pour garder ça simple, nous allons l’écrire en Java avec la librairie JavaFX.

Les données

Un courriel peut être représenté par les quelques informations suivantes :

  • Expéditeur
  • Destinataire
  • Sujet
  • Contenu

Vu que notre client est fait uniquement pour Manon, nous allons omettre le champ du destinataire.

Voici notre p’tit POJO (Plain Old Java Object en anglais) pour emmagasiner les données :

public class Courriel {
private final String expediteur;
private final String sujet;
private final String message;

public Courriel(String expediteur, String sujet, String message) {
this.expediteur = expediteur;
this.sujet = sujet;
this.message = message;
}

public String getExpediteur() {
return expediteur;
}

public String getSujet() {
return sujet;
}

public String getMessage() {
return message;
}
}

Le client

C’est avec un client que nous allons obtenir les courriels. Pour Manon, on va créer un “faux” client qui va attendre quelques secondes avant de lui faire parvenir une liste prédéterminée de courriels.

public class ClientCourriel {
private final long delaiAvantReponseEnMs;

public ClientCourriel(long delaiAvantReponseEnMs) {
this.delaiAvantReponseEnMs = delaiAvantReponseEnMs;
}

public List<Courriel> obtenirCourriels() {
// Manon ne le sait pas, mais on ne va pas vraiment chercher des courriels...
// On fait juste semblant
try {
Thread.sleep(delaiAvantReponseEnMs);
} catch (InterruptedException error) {
System.err.println("Euh... ça n'a pas marché");
}

return List.of(
new Courriel("Ricardo", "La meilleure recette de tarte", "Tu dois essayer cette recette!"),
new Courriel("Ginette Renaud", "Ma nouvelle chanson", "Salut. Peux-tu écouter mon dernier single et me dire ce que t'en penses? Merci!"),
new Courriel("Prince Nigérien", "Une fortune à partager", "Hey! Je dois partager mon cash. T'en veux? Donne moi ton numéro de compte, ton NAS, etc. pi je t'envois quelques millions!"));
}
}

Pourquoi? Premièrement, on ne sait même pas si Manon a une adresse courriel. Deuxièmement, c’est bien plus simple comme ça pour montrer comment exécuter du code sur un thread d’arrière plan.

Je suis certain que matante sera heureuse de recevoir une recette de Ricardo et écouter le dernier single de Ginette Renaud. Faudra l’avertir de se méfier du prince nigérien par contre…

L’interface utilisateur graphique

Pour afficher la liste de courriels, nous avons décidé d’utiliser un tableau. Ce dernier aura trois colonnes, soient une par champ. Le tableau sera créé avec la méthode suivante :

private TableView<Courriel> creerTableau() {
TableColumn<Courriel, String> colonneExpediteur = new TableColumn<>("Expéditeur");
colonneExpediteur.setCellValueFactory(caracteristiquesDesDonnees -> new SimpleObjectProperty<>(caracteristiquesDesDonnees.getValue().getExpediteur()));

TableColumn<Courriel, String> colonneSujet = new TableColumn<>("Sujet");
colonneSujet.setCellValueFactory(caracteristiquesDesDonnees -> new SimpleObjectProperty<>(caracteristiquesDesDonnees.getValue().getSujet()));

TableColumn<Courriel, String> colonneMessage = new TableColumn<>("Message");
colonneMessage.setCellValueFactory(caracteristiquesDesDonnees -> new SimpleObjectProperty<>(caracteristiquesDesDonnees.getValue().getMessage()));

TableView<Courriel> lesCourriels = new TableView<>();
List.of(colonneExpediteur, colonneSujet, colonneMessage).forEach(colonne -> lesCourriels.getColumns().add(colonne));

return lesCourriels;
}

Le contenu du tableau provient d’une liste observable. Quand le contenu de cette liste change, le tableau est rafraichi automatiquement.

ObservableList<Courriel> courrielsAffichees = FXCollections.observableArrayList();TableView<Courriel> lesCourriels = creerTableau();
lesCourriels.setItems(courrielsAffichees);

C’est avec notre client courriel que nous allons obtenir le contenu pour le mettre dans la liste. L’appel au client sera déclenché quand Manon pèse sur le piton.

Button lePiton = new Button("Télécharger les courriels");
lePiton.setOnAction(evenement -> courrielsAffichees.setAll(client.obtenirCourriels()));

Si on met tout ça ensemble, on obtient un client courriel bien simple pour Manon.

public class ClientCourrielPourManon extends Application {
private static final long DELAI_AVANT_REPONSE_EN_MS = 5000;

@Override
public void start(Stage primaryStage) {
ClientCourriel client = new ClientCourriel(DELAI_AVANT_REPONSE_EN_MS);

ObservableList<Courriel> courrielsAffichees = FXCollections.observableArrayList();

TableView<Courriel> lesCourriels = creerTableau();
lesCourriels.setItems(courrielsAffichees);
VBox.setVgrow(lesCourriels, Priority.ALWAYS);

Button lePiton = new Button("Télécharger les courriels");
lePiton.setOnAction(evenement -> courrielsAffichees.setAll(client.obtenirCourriels()));

primaryStage.setScene(new Scene(new VBox(lesCourriels, lePiton)));
primaryStage.setTitle("Client courriel pour Manon");
primaryStage.show();
}
}

L’application en action

Si on exécute l’application, la fenêtre suivante devrait apparaitre.

Un fenêtre qui contient un tableau de trois colonnes, soient une pour l’expéditeur, une pour le sujet et une pour le message, suivi d’un bouton qui permet de télécharger les courriels.
Le client courriel

Pas pire, hein? Pèses dont sur le piton.

Une image animée de l’application. Après avoir peser sur le bouton pour obtenir les courriels, lorsque le bouton ou les titres des colonnes du tableau sont pesés, l’application ne répond pas. Suite au délai de trois secondes, l’application répond à nouveau.
Le client courriel est gelé le temps que les courriels sont “téléchargés”

L’application est complètement gelée le temps que notre client courriel travaille pour obtenir les données du serveur.

Comment se fait-il?

Imagines un restaurant avec un seul employé. Cette personne doit prendre les commandes, cuisiner les repas et les servir. Il est fort probable que les clients devront attendre longtemps avant de recevoir leur plat.

Le restaurant et son seul employé représentent l’application et son thread principal, soit celui de JavaFX.

L’application doit réagir à l’événement qui est lancé quand on clique sur le bouton, obtenir les courriels et redessiner les composantes du GUI une fois qu’il a rempli le tableau. Il ne peut pas faire plus qu’une chose à la fois. C’est pour ça que pendant que l’application obtient les courriels il n’a pas l’air de répondre. Tout débloque une fois que l’opération est terminée.

Si l’application est gelée trop longtemps, Manon risque de péter une coche ou deux!

On ne veut pas choquer Manon.

Comment peut-on éviter de geler l’application? En effectuant la requête sur un thread d’arrière plan.

L’antigel

En Java, une des façons les plus simples et élégantes de lancer une opération sur un thread d’arrière plan est d’utiliser un CompletableFuture.

Voici comment on peut modifier l’action du bouton pour effectuer l’appel au client sur un thread d’arrière plan et revenir sur le thread principal pour mettre à jour le contenu du tableau.

lePiton.setOnAction(evenement -> CompletableFuture.supplyAsync(client::obtenirCourriels)
.thenAcceptAsync(courrielsAffichees::setAll, Platform::runLater));

Décortiquons l’appel.

CompletableFuture.supplyAsync(client::obtenirCourriels);

La méthode statique supplyAsync indique à l’application que la méthode obtenirCourriels de notre client va lui fournir la liste de courriels. L’appel à la méthode se fait de façon asynchrone, soit sur un thread d’arrière plan.

Le retour de la méthode supplyAsync est un CompletableFuture. Cet objet représente une valeur qui sera disponible à l’avenir. Lorsque l’opération est terminée, le futur est considéré complet.

Le contenu du futur une fois complété sera la liste de courriels dans notre cas.

CompletableFuture<List<Courriel>> futursCourriels = CompletableFuture.supplyAsync(client::obtenirCourriels);

Il est impossible de prévoir quand les courriels seront disponibles, mais une fois qu’ils le sont nous voulons les afficher dans le tableau.

Avec la méthode thenAcceptAsync du CompletableFuture, on indique à l’application de prendre la liste de courriels et les mettre dans la liste de courriels à afficher. Cette méthode accepte le résultat de l’appel précédent, soit le supplyAsync.

CompletableFuture.supplyAsync(client::obtenirCourriels)
.thenAcceptAsync(courrielsAffichees::setAll, Platform::runLater);

Lorsque le contenu de la liste des courriels à afficher est modifié, le tableau est redessiné.

Toute opération qui engendre une modification au GUI doit être effectuée dans le thread de JavaFX. C’est pour cette raison qu’on passe Platform::runLater comme deuxième paramètre. Ce dernier fait en sorte que l’opération sera effectuée sur le thread principal de l’application.

Si on exécute l’application suite à nos modifications, on obtient un bien meilleur résultat.

Une image animée de l’application. Après avoir peser sur le bouton pour obtenir les courriels, lorsque le bouton ou les titres des colonnes du tableau sont pesés, l’application répond. À la fin, les courriels apparaissent dans le tableau.
Le client courriel répond aux interactions de l’utilisateur le temps que les courriels sont “téléchargés”

Tout va comme sur des roulettes.

L’application utilise maintenant deux threads. Le thread principal de JavaFX s’occupe des interactions de l’utilisateur et de l’affichage des données à l’écran tandis qu’un thread d’arrière plan se charge d’obtenir les courriels.

C’est un restaurant avec deux employés. Le serveur s’occupe de prendre les commandes et de servir les plats tandis que le cuisinier se charge de préparer la nourriture.

Et voilà!

On a créé un client courriel pour Manon. Si jamais on crée une adresse courriel pour notre chère tante, on pourra communiquer avec l’API du serveur sur le web sans inquiétudes. Même si elle a une mauvaise connexion Internet, le GUI ne sera pas gelé le temps qu’on obtient ses courriels.

Aller Manon. Pèse sur le piton.

Le code source du client courriel est disponible dans le dépôt suivant sur GitHub : https://github.com/leblancjs/client-courriel-pour-manon.

--

--