• 10 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 29/08/2024

Découvrez les Threads

La première phase du projet Epicrafter’s Journey est proche de se terminer. Le code que votre équipe devait livrer est complet et respecte le schéma initialement prévu à savoir :

Diagramme montrant des classes, des interfaces et des énumérations avec leurs relations
Modélisation finale du projet

Tandis que Jenny s’affaire à la préparation des prochaines fonctionnalités à implémenter et que Marc prépare les prochaines échéances techniques, Juan exécute une nouvelle batterie de tests pour éprouver la solution.

Juan passe vous voir :

Je viens de terminer mes tests, fonctionnellement le programme respecte l’attendu. Je n’ai pas trouvé d’erreurs qui ne sont pas gérées. Par contre, nous avons un problème de performance. Le kit de démarrage prend plusieurs secondes à se créer, il faut améliorer ça !

Quelque peu surpris, vous sollicitez Marc pour avoir son avis.

Ne t’inquiète pas, on va tirer profit des threads en Java pour corriger ça.

Gérez la performance avec la programmation concurrente

La thématique de la performance est cruciale ! Accepteriez-vous en tant qu’utilisateur qu’une application ait besoin de plusieurs secondes pour afficher le résultat d’un traitement ? Très difficilement !

Cela est dû au fait qu’aujourd’hui les technologies matérielles et logicielles nécessaires existent pour permettre à nos applications d’être performante au niveau du temps d’exécution.

Alors comment est-ce possible d’avoir des problèmes de performance avec Java 21 ?

Généralement à cause de l’exécution séquentielle du code. Une exécution séquentielle signifie que les traitements seront exécutés les uns après les autres. Le temps d’exécution du programme correspond donc à la somme du temps de traitement de chaque traitement.

Reprenons le cas de notre projet Epicrafter’s Journey. La classe Main va instancier 9 blocs successivement. Chaque construction de bloc prendra au moins 1 seconde, il faudra donc 9 secondes pour construire tous les blocs.

Notre solution repose sur une exécution concurrente du code. Autrement dit, la construction des différents blocs doit être faite en simultanée, en parallèle.

Java exécute le code au sein de thread. Un thread est un ensemble de traitement. Les systèmes d’exploitation sont en mesure de paralléliser l’exécution des threads, d’autant plus avec les postes de travail qui sont multicœurs aujourd’hui.

Donc pour rendre plus performant notre programme, nous devrons utiliser un ensemble de thread (thread pool en anglais) dédié à la parallélisation des traitements coûteux en temps. Il existe une interface java.concurrent.ExecutorService qui nous permettra de gérer ces pools. 

Voyez comment mettre cela en œuvre !

Écrivez un code concurrent

Dans cette démonstration, je vous montre l’utilisation d’un pool de 10 threads pour construire les blocs :

Vraiment bien n’est-ce pas ? Au lieu d’environ 9 secondes, le code met environ 1 seconde à s’exécuter, nous avons été 9 fois plus rapides en parallélisant la création des blocs !

Que pouvez-vous retenir de la démonstration ?

  • La classe java.concurrent.Executors nous permet de créer différents types de thread pool. Ici nous avons choisi un thread pool fixé à 10 threads.

  • L’interface fonctionnelle java.util.concurrent.Callable permet de définir une tâche qui renvoie un résultat et sera exécutée grâce au thread pool.

  • L’interface java.util.concurrent.Future sera le type retour après l’exécution d’une tâche qui renvoie un résultat dans un thread pool.

Voici le code que nous avons traité, je vous mets uniquement la méthode constructionSetBlocs de la classe Main :

private static Set<IBloc> constructionSetBlocs() throws IllegalBlocException {
    
    Set<IBloc> blocs = new LinkedHashSet<IBloc>();
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    
    Callable<IBloc> taskMur1 = () -> { return new Mur(3, 2, 2, true); };
    Callable<IBloc> taskMur2 = () -> { return new Mur(2, 1, 2, false); };
    Callable<IBloc> taskPorte = () -> { return new Porte(1, 2, 2, true); };
    Callable<IBloc> taskToit = () -> { return new Toit(3, 1, 1); };
    
    List<Callable<IBloc>> tasks = Arrays.asList(taskMur1, taskMur1, taskMur2, taskMur2, taskPorte, taskToit);
    try {
        List<Future<IBloc>> resultas = executorService.invokeAll(tasks);
        resultas.forEach((resultat) -> {
        try {
            blocs.add(resultat.get());
        } catch (InterruptedException | ExecutionException e) {
            logger.error("Erreur lors de création parallèle des blocs.");
        }
        });
    } catch (InterruptedException e) {
        logger.error("Erreur lors de création parallèle des blocs.");
    }
    executorService.shutdown();
    return blocs;
}

Nous avons résolu le problème de performance identifié par Juan. Sachez qu’à partir de Java 21, de nouvelles possibilités sont disponibles, explorons cela !

Utilisez les Virtual Threads grâce à Java 21

Depuis la version 21 de Java, pour affronter les problèmes de concurrence, les Virtual Thread sont à votre disposition.

La notion de thread vu dans la section précédente peut être renommée Platform Thread.

Les Platform Thread sont directement gérés par le système d’exploitation, là où les Virtual Thread sont gérés par la JVM.

Grâce à cela, les Virtual Thread sont plus légers et indépendants du contexte du système d’exploitation. Il est donc possible pour votre programme Java d’en utiliser beaucoup plus !

 

Gestionnaire du Thread

Conséquence

Platform Thread

Système d’exploitation

Sollicite directement l’OS donc plus lourd

Virtual Thread

JVM

Ne sollicite pas l’OS et donc plus léger.

Dans la démonstration suivante, je vous montre comment les mettre à l’œuvre:

Que pouvons-nous retenir ?

  • Sur une petite échelle, nous ne voyons pas de différence de performance. Cela sera notable lorsque beaucoup de thread sont utilisés.

  • Leur mise en œuvre ne change pas l’écriture des tâches à exécuter mais uniquement la façon de créer l’objet ExecutorService.

Sachez que c’est à partir des 10 000 blocs que les Virtual Thread étaient significativement plus rapides sur mon poste de travail. Je vous ai d’ailleurs préparé un petit graphique :

Test de performance
Test de performance 

Le code de la méthode constructionSetBlocs de la classe Main est le suivant :

private static Set<IBloc> constructionSetBlocs() throws IllegalBlocException {
    Set<IBloc> blocs = new LinkedHashSet<IBloc>();
    ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
    
    List<Callable<IBloc>> tasks = new ArrayList<Callable<IBloc>>();
    for(int i=0; i<10000; i++) {
        Callable<IBloc> taskMur = () -> { return new Mur(3, 2, 2, true); };
        tasks.add(taskMur);
    }
    try {
        List<Future<IBloc>> results = executorService.invokeAll(tasks);
        results.forEach((result) -> {
            try {
                blocs.add(result.get());
            } catch (InterruptedException | ExecutionException e) {
                logger.error("Erreur lors de création parallèle des blocs.");
            }
        });
    } catch (InterruptedException e) {
        logger.error("Erreur lors de création parallèle des blocs.");
    }
    
    executorService.shutdown();
    return blocs;
}

La ligne clé de ce code est  ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();  qui permet de mettre en œuvre des Virtual Thread.

Avant de conclure j’aimerais mettre en évidence que les Virtual Thread ne s’exécutent pas plus rapidement que les Platform Thread. Par contre ils sont créés et gérés plus rapidement par la JVM que les Platform Thread.

À vous de jouer

Contexte

La problématique de la performance qui a été résolue dans ce chapitre a donné une idée à Marc. Il souhaite inclure une nouvelle fonctionnalité :

ID de la tâche : 10

Nom de la tâche : Fabrique de blocs

Description de la tâche : 

Une classe doit être créée permettant de créer autant de blocs que souhaités.

La classe possède une méthode de création par type de blocs : creerMur, creerPorte, creerToit.

Les blocs seront créés avec une valeur à 1 pour la longueur, la hauteur et la largeur. Les murs seront porteurs et les portes non verrouillées.

Vous vous lancez dans cette nouvelle tâche !

Consignes

  1. Créez une classe Fabrique dans le package ej.blocs.

  2. Créez une méthode statique de visibilité public, qui renvoie une liste de IBloc, de nom creerMur et qui prend en paramètre la quantité de bloc à créer.

  3. Implémentez la méthode creerMur en utilisant les Virtual Thread pour instancier les blocs tout en gérant la question de la performance.

  4. Répétez les 2 précédentes étapes pour creerPorte et creerToit.

En résumé

  • Pour résoudre les problématiques de performance associées à la programmation séquentielle, Java permet d’exécuter le code de façon concurrente.

  • Un Thread représente un ensemble de traitement.

  • Les Platform Thread sont gérés par le système d’exploitation.

  • Les Virtual Thread sont gérés par la JVM et sont plus rapides à créer que les Platform Thread.

  • Indépendamment du type de Thread, l’interface ExecutorService nous aide à mettre en place des Thread et donc une exécution concurrente.

Je tiens sincèrement à vous féliciter pour avoir suivi ce cours jusqu’au bout ! Bien que de nombreuses choses vous attendent encore dans l’univers Java, les connaissances et compétences acquises dans ce cours vous offrent une solide assise pour la suite de votre progression. Puissiez-vous continuer de progresser avec le langage de programmation Java !

Exemple de certificat de réussite
Exemple de certificat de réussite