Jeudi 23 janvier 2025
Optimisez vos applications Java grâce au Multithreading
Thread Scheduler, synchronized, Executors, Runnable ou encore Callable... Tous ces mots vous disent sûrement quelque chose, mais peut-être n’êtes-vous pas très habitué à les côtoyer dans vos applications.
De nos jours, la performance et le traitement en temps réel sont devenus des enjeux essentiels pour répondre aux attentes des utilisateurs. Il est donc plus que jamais utile pour les développeurs Java de comprendre comment fonctionne le traitement en parallèle.
Qu'est-ce que le multithreading ?
Le multithreading est une technique qui permet à une application d'exécuter plusieurs tâches en même temps.Pourquoi ?
En traitant plusieurs tâches en parallèle plutôt qu'une par une, le multithreading permet un gain de temps considérable, idéal pour des applications exigeantes ! Dans cet article, nous allons explorer les bases du multithreading en Java. Si vous n’avez jamais travaillé avec les threads, je ne prétends pas faire de vous des experts après la lecture de cet article, mais au moins vous aurez les clés pour vous lancer dans le traitement en parallèle. 😉C’est quoi un thread ?
Commençons par un peu de théorie sans trop entrer dans les détails. Un thread est une petite unité de travail qui peut effectuer une tâche spécifique dans un programme. C’est le système d’exploitation qui va les gérer pour les mapper avec un cœur logique pour y exécuter des instructions. Pour les développeurs Java, cette partie est très souvent opaque. Ils utilisent des abstractions comme les classes Thread ou Runnable, qui sont gérées par la JVM. La JVM s’appuie sur un orchestrateur de threads, le Thread Scheduler, qui transmet les threads au système d’exploitation. Ce dernier les associe à des threads natifs, que le processeur utilise pour exécuter les tâches (temps CPU).Les Concepts Fondamentaux du Multithreading en Java
Pour bien comprendre le multithreading en Java, il est essentiel de connaître les principaux concepts et objets utilisés pour gérer les threads, ainsi que le service responsable de leur exécution.Les Classes et Interfaces Fondamentales
1. Classe Thread
La classe Thread est au cœur de la création et de la gestion des threads en Java. Chaque instance de Thread représente un thread d'exécution distinct. Pour créer un thread, il suffit de créer une instance de Thread et d’appeler sa méthode start() pour démarrer son exécution.2. L'Interface Runnable
Runnable est une interface fonctionnelle qui définit une seule méthode, run(). Elle permet de créer des threads en séparant la logique de la gestion des threads. Cette approche est souvent privilégiée car elle permet d'implémenter d’autres interfaces ou d’hériter d’autres classes.3. L’Interface Callable
Callable est une interface fonctionnelle qui définit une seule méthode, call(), capable de retourner un résultat et de lever des “checked exceptions”. Elle est souvent utilisée avec un ExecutorService pour exécuter des tâches asynchrones et récupérer leur résultat sous forme d'un objet Future. Cette approche est idéale pour les scénarios où une tâche nécessite un retour ou une gestion avancée des erreurs. La plupart des frameworks modernes de gestion des threads, comme ExecutorService, utilisent des instances de Runnable ou Callable, et pas des enfants de Thread. Cependant, l’utilisation de cette classe peut parfois être intéressante.Gestion des threads
1. Executors
Gérer les threads manuellement dans une application peut rapidement devenir complexe : risque de surcréation, gestion difficile de leurs états et ralentissement du système en cas de surcharge. Les exécuteurs simplifient cette tâche en réutilisant des threads existants et en gérant automatiquement leur cycle de vie, rendant le processus plus efficace et maîtrisé. En Java, les méthodes statiques de la classe Executors permettent de créer facilement différents types d'exécuteurs. Plus besoin de configurer manuellement les pools de threads : elles permettent d’obtenir un exécuteur adapté aux besoins comme ExecutorsService ou encore ScheduledExecutorService. ExecutorService:- submit() : Soumet une tâche (Runnable ou Callable) et retourne un Future pour suivre son état.
- shutdown() : Arrête l'exécuteur après la fin des tâches en cours.
- shutdownNow() : Arrête immédiatement toutes les tâches.
- newCachedThreadPool() : Adapté aux tâches courtes ou imprévisibles, crée des threads selon les besoins et les réutilise.
- newSingleThreadExecutor() : Pour exécuter des tâches dans un ordre séquentiel avec un seul thread.
- newScheduledThreadPool() : Idéal pour planifier des tâches répétitives ou différées à l’aide du ScheduledExecutorService.
2. Synchronisation avec synchronized
Lorsque plusieurs threads accèdent aux mêmes ressources, il est crucial de gérer cet accès pour éviter les “race condition”. Le mot-clé synchronized permet de verrouiller une méthode ou un bloc de code afin que seul un thread puisse y accéder à la fois. Dans cet exemple, la méthode incrément du compteur est synchronisée, garantissant qu’un seul thread peut exécuter cette méthode à la fois.3. Thread Scheduler
Comme annoncé plus haut, les threads sont gérés par le Thread Scheduler, le service de la JVM qui attribue du temps de CPU aux threads actifs. Le scheduler détermine l'ordre d'exécution des threads et gère leurs états :- NEW : Le thread a été créé mais n'a pas encore démarré. Le thread est dans cet état après avoir été instancié avec new Thread() mais avant l'appel à start().
- RUNNABLE: Le thread est prêt à s'exécuter mais attend que le CPU lui soit attribué. L'état RUNNABLE ne signifie pas que le thread est en cours d'exécution, mais qu'il est éligible pour être exécuté par le CPU.
- BLOCKED: Le thread est bloqué en attente d'une ressource qu'un autre thread détient. Cela survient uniquement lorsqu'un thread tente d'accéder à un bloc ou une méthode synchronized et que le verrou est détenu par un autre thread.
- WAITING: Le thread est suspendu en attente d’un signal ou d’un événement. Cet état se produit principalement avec wait(), join().
- TIME_WAITING : Le thread est suspendu pendant un temps donné. Cet état est utilisé lorsque le thread attend avec un délai spécifié, comme dans sleep(milliseconds), wait(timeout) ou join(timeout).
- TERMINATED: Le thread a terminé son exécution. Cet état indique que la méthode run() du thread s'est terminée (avec succès ou suite à une interruption). Un thread dans cet état ne peut pas être redémarré.
- Création de threads avec Thread.
- Implémentation de tâches via Runnable ou Callable.
- Synchronisation pour protéger les ressources partagé
- Gestion des tâches parallèles avec les exécuteurs (Executor, ExecutorService) et les pools de threads.
- Thread Scheduler pour comprendre les états des threads.
- Collections concurrentes : Classes comme ConcurrentHashMap, CopyOnWriteArrayList, et BlockingQueue pour gérer des structures de données partagées en toute sécurité.
- Synchronizers (CountDownLatch, CyclicBarrier, Semaphore) pour coordonner plusieurs threads.
- Classes atomiques dans util.concurrent.atomic pour des opérations thread-safe sans verrouillage.
- Locks avancés comme ReentrantLock et ReadWriteLock pour un contrôle pré
- Fork/Join Framework pour diviser des tâches lourdes en sous-tâ
- Outils réactifs comme RxJava, Akka, et x pour une approche moderne et asynchrone.
- Virtual Threads (Project Loom) pour une gestion ultra-légère et performante des threads.
Par Clément.