Création et gestion des threads en Java
Java offre plusieurs mécanismes pour implémenter le multithreading. Voici cinq approches courantes :
- Implémentation de l'interface Runnable : On définit la méthode
run()dans une classe qui implémente Runnable, puis on passe une instance à un objet Thread pour démarrer l'exécution.
Exemple :Thread monFil = new Thread(new MaTache()); monFil.start(); - Extension de la classe Thread : On crée une classe qui hérite de Thread et on surcharge la méthode
run(). L'instance peut être démarrée directement.
Exemple :MaClasseDeFil instanceFil = new MaClasseDeFil(); instanceFil.start(); - Utilisation de Callable avec FutureTask : On implémente l'interface Callable pour retourner un résultat, on l'enveloppe dans un FutureTask, puis on le lance via un Thread.
Exemple :FutureTask<String> tacheFuture = new FutureTask<>(new MonCallable()); Thread fil = new Thread(tacheFuture); fil.start(); - Recours à un pool de threads (ExecutorService) : On soumet des tâches Runnable ou Callable à un ExecutorService, ce qui évite la création directe de threads et facilite la gestion des tâches concurrentes.
Exemple :ExecutorService executeur = Executors.newFixedThreadPool(5); executeur.execute(new MaTache()); - Utilisation de CompletableFuture (basé sur un pool interne) : Introduit en Java 8, il permet d'exécuter des tâches asynchronse et de chaîner des opérations avec des méthodes comme
thenApply.
Exemple :CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { /* logique */ });
Cycle de vie et états d'un thread
Un thread en Java traverse plusieurs états au cours de son exécution :
- Initialisé (New) : Lorsqu'il est créé avec
new Thread(). - Prêt (Runnable) : Après l'appel à
start(), il attend l'allocation de temps processeur. - En cours d'exécution (Runing) : Quand il obtient le CPU et exécute la méthode
run(). - Bloqué (Blocked) : Si le thread perd le CPU, par exemple en attendant un verrou ou une ressource I/O. Cela inclut les blocages d'attente (via
wait()), de synchronisation (échec d'acquisition de verrou), ou temporaires (commesleep()). - Terminé (Terminated) : Lorsque la tâche se termine normalement ou via une interruption/exception.
Méthodes essentielles pour les threads
Voici quelques méthodes clés de la classe Thread et Object pour contrôler les threads :
wait(): Met le thread en attente et libère le verrou sur l'objet.sleep(long millis): Suspend le thread pour une durée donnée sans libérer les verrous.yield(): Cède le CPU à d'autres threads de priorité similaire.interrupt(): Interrompt un thread en modifiant son drapeau d'interruption ; les threads bloqués lèvent uneInterruptedException.join(): Attend la terminaison d'un autre thread.notify()etnotifyAll(): Réveillent un ou tous les threads en attente sur un objet.
Pools de threads : concepts et configuration
Un pool de threads réutilise un ensemble de threads pour exécuter des tâches, réduisant ainsi le coût de création et destruction. En Java, on utilise principalement ThreadPoolExecutor pour une configuration personnalisée.
Paramètres importants :
- corePoolSize : Nombre minimum de threads maintenus actifs.
- maximumPoolSize : Nombre maximal de threads pouvant être créés.
- keepAliveTime : Durée pendant laquelle les threads excédentaires restent en attente avant d'être terminés.
- workQueue : File d'attente pour les tâches en attente d'exécution (par exemple
ArrayBlockingQueueouLinkedBlockingQueue). - handler : Stratégie de rejet lorsque le pool et la file sont saturés (par exemple,
CallerRunsPolicy).
Exemple de création d'un pool :
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100)
);
Avantages des pools : réutilisation des threads, contrôle de la concurrence, gestion intégrée et file d'atente pour les tâches. Inconvénients : nécessité de configuration adaptée et risques potentiels de fuites ou deadlocks.
Haute concurrence : métriques et solutions
Les systèmes à haute concurrence requièrent une surveillance attentive des indicateurs clés :
- QPS (Requêtes par seconde) : Volume de requêtes traitées par seconde.
- TPS (Transactions par seconde) : Nombre de transactions complétées par seconde.
- Temps de réponse (RT) : Délai moyen pour traiter une requête.
- Nombre de connexions simultanées : Capacité à gérer des requêtes en parallèle.
Pour atteindre une haute performance et disponibilité, plusieurs stratégies sont employées :
- Architecture microservices et répartition de charge : Décomposer le système en services indépendants et répartir les requêtes via des équilibreurs de charge (algorithme round-robin, connexions minimales, etc.).
- Mise en cache distribuée : Utiliser des solutions comme Redis ou Memcached pour accélérer les lectures.
- Files de messages (MQ) : Absorber les pics d'écriture avec des outils comme RabbitMQ ou Kafka.
- Partitionnement de bases de données : Répartir les données horizontalement ou verticalement pour améliorer les performances.
- Lecture/écriture séparées : Diriger les écritures vers une base principale et les lectures vers des replicas.
- Limitation de débit et coupures de circuit : Protéger le système contre la surcharge avec des mécanismes de fusion (circuit breaker).
- Séparation statique/dynamique : Différencier le contenu statique et les requêtes dynamiques pour optimiser les ressources.
- Optimisation des bases de données : Améliorer les index et les requêtes SQL.
Transmission de données entre threads asynchrones
Pour partager le contexte (comme les informations d'authentification) entre threads parents et enfants, plusieurs techniques existent :
- Passage manuel : Récupérer la valeur du thread parent et la définir dans le thread enfant via
ThreadLocal. - Décorateur de tâche (TaskDecorator) : Configurer un pool de threads avec un décorateur qui propage automatiquement le contexte.
- InheritableThreadLocal : Étend
ThreadLocalpour hériter des valeurs dans les threads enfants, mais avec des limites dans les pools de threads. - TransmittableThreadLocal : Une bibliothèque open-source (par Alibaba) qui assure une transmission fiable dans les environnements de threads réutilisables.
Exemple de décorateur de tâche :
public class DecorateurContexte implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
LoginInfo info = ContexteAuth.get(); // Récupérer le contexte du thread parent
return () -> {
try {
ContexteAuth.set(info); // Propager au thread enfant
runnable.run();
} finally {
ContexteAuth.clear(); // Nettoyer après exécution
}
};
}
}