Maîtriser les Pools de Threads Java : Guide Pratique

L'utilisation directe de threads est souvent inefficace dans les applications concurrentes. Un pool de threads offre plusieurs avantages critiques :

  • Optimisation des ressources : Évite la surcharge système liée à la création et destruction répétée de threads.
  • Contrôle de la concurrence : Limite le nombre de tâches simultanées pour prévenir l'épuisement des ressources serveur.
  • Gestion centralisée : Facilite le monitoring et la maintenance du cycle de vie des exécutants.

Mise en œuvre basique

Pour initier une infrastructure de traitement concurrentiel, on instancie généralement un objet ThreadPoolExecutor. Voici un exemple configuré manuellement plutôt que par une fabrique :

import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ApplicationConcurrente {

    public static void main(String[] args) {
        // Définition des constantes de configuration
        int MINIMUM_THREADED = 4;
        int MAXIMUM_THREADED = 8;
        long TIMEOUT_ACTIF = 60L;

        // Création explicite du gestionnaire de tâches
        ExecutorService gestionnaireTaches = new ThreadPoolExecutor(
            MINIMUM_THREADED,
            MAXIMUM_THREADED,
            TIMEOUT_ACTIF,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100), // File d'attente bornée
            new ThreadPoolExecutor.CallerRunsPolicy() // Stratégie par défaut alternative
        );

        try {
            // Soumission d'une tâche
            gestionnaireTaches.submit(() -> {
                String nomFil = Thread.currentThread().getName();
                System.out.println("Le fil [" + nomFil + "] traite la demande.");
            });
        } finally {
            gestionnaireTaches.shutdown();
        }
    }
}

Analyse détaillée des paramètres du constructeur

La classe ThreadPoolExecutor expose sept arguments principaux qui déterminent son comportement.

1. corePoolSize (Taille du noyau)

Définit le nombre minimum de threads actifs qui persistent même s'ils sont inactifs, sauf si le mode de recyclage est activé.

2. maximumPoolSize (Capacité maximale)

Somme totale autorisée incluant les threads du noyau et les threads temporaires créés sous forte charge.

3. keepAliveTime & 4. unit (Durée de maintien)

Période durant laquelle les threads excédentaires (non-noyaux) peuvent rester inactifs avant d'être supprimés. L'unité temporelle peut être définie en secondes, millisecondes, etc.

5. workQueue (File de travail)

Conteneur stockant les tâches soumises lorsque le noyau est saturé. Les implémentations courantes incluent :

  • LinkedBlockingQueue : File liée, capacité potentiellement illimitée ou réglable.
  • ArrayBlockingQueue : Tableau circulaire, taille fixe imposée à la création.
  • SynchronousQueue : Aucune mise en cache réelle, chaque insertion exige un retrait immédiat.
  • DelayedWorkQueue : Permet d'exécuter des tâches selon un délai futur spécifique.

6. threadFactory (Usine de threads)

Interface permettant de personnaliser la création des instances de threads (nommage, priorité, daemon). Par défaut, Java utilise une usine interne générant des noms comme pool-1-thread-1.

7. handler (Gestionnaire de refus)

Intervient lorsque le pool et sa file sont pleins. Les stratégies standards sont :

  • AbortPolicy : Lance une exception RejectedExecutionException.
  • DiscardPolicy : Supprime simplement la tâche sans notification.
  • DiscardOldestPolicy : Retire la tâche la plus ancienne de la file pour faire place à la nouvelle.
  • CallerRunsPolicy : Exécute la tâche dans le thread ayant effectué l'appel.

Cycle de traitement d'une tâche

Lorsqu'un client soumet une instruction au gestionnaire, la logique suivante s'applique séquentiellement :

  1. Vérification initiale : Si le nombre courant de threads est inférieur à corePoolSize, un nouveau thread est immédiatement créé pour traiter la requête.
  2. Mise en file d'attente : Une fois la taille minimale atteinte, la tâche est placée dans workQueue en attendant qu'un thread du noyau devienne disponible.
  3. Création temporaire : Si la file est pleine et que le total des threads est inférieur à maximumPoolSize, un thread supplémentaire (hors noyau) est instantané créé.
  4. Rejet : Si la file est saturée et que le nombre maximal de threads est atteint, la politique de refus définie par handler est déclenchée.

État interne du pool

Le cycle de vie d'un pool est régi par cinq états internes gérés par une machine à états finie :

  • RUNNING : État initial. Le pool accepte les nouvelles tâches et exécute celles en attente.
  • SHUTDOWN : Déclenché par shutdown(). Aucun nouveau job n'est accepté, mais les tâches en cours et en file poursuivent leur exécution.
  • STOP : Déclenché par shutdownNow(). Tous les threads sont interrompus brutalement et les jobs non démarrés sont éliminés.
  • TIDYING : État transitoire où tous les travaux sont achevés et les threads terminés, en attente de l'exécution de la méthode terminated().
  • TERMINATED : État final après l'achèvement complet du nettoyage du pool.

Usines standard fournies par JDK

La classe utilitaire Executors propose des méthodes abrégées pour configurer des pools courants :

newCachedThreadPool

Idéal pour une charge variable avec beaucoup de petites tâches. Il crée dynamiquement des threads si nécessaire, mais recycle ceux qui restent inutilisés pendant 60 secondes.

ExecutorService poolFlexible = Executors.newCachedThreadPool();

newFixedThreadPool

Crée un ensemble fixe de threads réutilisables. La file d'attente utilisée est LinkedBlockingQueue de capacité illimitée par défaut, ce qui nécessite une surviellance pour éviter l'épuisement mémoire.

ExecutorService poolFixe = Executors.newFixedThreadPool(nombresThreads);

newSingleThreadExecutor

Garantit l'exécution séquentielle stricte des tâches. Contrairement à un FixedThreadPool(1), la configuration interne est encapsulée et ne peut pas être modifiée accidentellement par le développeur.

ExecutorService sequenceUnique = Executors.newSingleThreadExecutor();

newScheduledThreadPool

Extension spéciale supportant l'exécution différée ou périodique. Elle utilise DelayedWorkQueue pour gérer les temporisations internes.

ScheduledExecutorService planificateur = Executors.newScheduledThreadPool(tailleNoyau);

Étiquettes: Java multithreading Concurrency JVM ThreadPoolExecutor

Publié le 8 août à 21h53