Un autre type de pool de threads est "caché". Ces pools de threads sont tout aussi couramment utilisés que les pools fixes.

Comme son nom l'indique, ce type de pool de threads met en cache les threads. Il maintient les threads inutilisés en vie pendant un temps limité afin de les réutiliser pour effectuer de nouvelles tâches. Un tel pool de threads est idéal lorsque nous avons une quantité raisonnable de travail léger.

Le sens de "une quantité raisonnable" est assez large, mais vous devez savoir qu'un tel pool n'est pas adapté à toutes les tâches. Par exemple, supposons que nous voulions créer un million de tâches. Même si chacun prend très peu de temps, nous utiliserons toujours une quantité déraisonnable de ressources et dégraderons les performances. Nous devons également éviter de tels pools lorsque le temps d'exécution est imprévisible, par exemple avec des tâches d'E/S.

Sous le capot, le constructeur ThreadPoolExecutor est appelé avec les arguments suivants :


public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, 
      new SynchronousQueue<Runnable>());
}

Les valeurs suivantes sont transmises au constructeur en tant qu'arguments :

Paramètre Valeur
corePoolSize (combien de threads seront prêts (démarrés) au démarrage du service d'exécuteur ) 0
maximumPoolSize (le nombre maximum de threads qu'un service d'exécuteur peut créer) Entier.MAX_VALUE
keepAliveTime (le temps qu'un thread libéré continuera à vivre avant d'être détruit si le nombre de threads est supérieur à corePoolSize ) 60L
unité (unités de temps) TimeUnit.SECONDS
workQueue (mise en place d'une file d'attente) new SynchronousQueue<Runnable>()

Et nous pouvons passer notre propre implémentation de ThreadFactory exactement de la même manière.

Parlons de SynchronousQueue

L'idée de base d'un transfert synchrone est assez simple et pourtant contre-intuitive (c'est-à-dire que l'intuition ou le bon sens vous dit que c'est faux) : vous pouvez ajouter un élément à une file d'attente si et seulement si un autre thread reçoit l'élément à la en même temps. En d'autres termes, une file d'attente synchrone ne peut pas contenir de tâches, car dès qu'une nouvelle tâche arrive, le thread d'exécution a déjà récupéré la tâche .

Lorsqu'une nouvelle tâche entre dans la file d'attente, s'il existe un thread actif libre dans le pool, il récupère la tâche. Si tous les threads sont occupés, un nouveau thread est créé.

Un pool mis en cache commence avec zéro thread et peut potentiellement atteindre des threads Integer.MAX_VALUE . Essentiellement, la taille d'un pool de threads mis en cache n'est limitée que par les ressources système.

Pour économiser les ressources système, les pools de threads mis en cache suppriment les threads inactifs pendant une minute.

Voyons comment cela fonctionne en pratique. Nous allons créer une classe de tâche qui modélise une requête utilisateur :


public class Task implements Runnable {
   int taskNumber;

   public Task(int taskNumber) {
       this.taskNumber = taskNumber;
   }

   @Override
   public void run() {
       System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
   }
}
    

Dans la méthode principale , nous créons newCachedThreadPool puis ajoutons 3 tâches à exécuter. Ici, nous imprimons l'état de notre service (1) .

Ensuite, nous faisons une pause de 30 secondes, démarrons une autre tâche et affichons le statut (2) .

Après cela, nous suspendons notre fil principal pendant 70 secondes, imprimons le statut (3) , puis ajoutons à nouveau 3 tâches et imprimons à nouveau le statut (4) .

Aux endroits où nous affichons l'état immédiatement après l'ajout d'une tâche, nous ajoutons d'abord une mise en veille d'une seconde pour une sortie à jour.


ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            executorService.submit(new Task(i));
        }
 
        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(1)
 
        TimeUnit.SECONDS.sleep(30);
 
        executorService.submit(new Task(3));
        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(2)
 
        TimeUnit.SECONDS.sleep(70);
 
            System.out.println(executorService);	//(3)
 
        for (int i = 4; i < 7; i++) {
            executorService.submit(new Task(i));
        }
 
        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(4)
        executorService.shutdown();

Et voici le résultat :

Demande utilisateur traitée n° 0 sur le thread pool-1-thread-1
Demande utilisateur traitée n° 1 sur le thread pool-1-thread-2
Demande utilisateur traitée n° 2 sur le thread pool-1-thread-3
(1) java.util.concurrent .ThreadPoolExecutor@f6f4d33[En cours d'exécution, taille du pool = 3, threads actifs = 0, tâches en file d'attente = 0, tâches terminées = 3]
Demande d'utilisateur traitée #3 sur pool-1-thread-2 thread
(2) java.util.concurrent. ThreadPoolExecutor@f6f4d33[En cours d'exécution, taille du pool = 3, threads actifs = 0, tâches en file d'attente = 0, tâches terminées = 4] (3) java.util.concurrent.ThreadPoolExecutor@f6f4d33[En cours d'exécution, taille du pool = 0
, threads actifs = 0 , tâches en file d'attente = 0, tâches terminées = 4]
Requête utilisateur traitée n° 4 sur le thread pool-1-thread-4
Requête utilisateur traitée n° 5 sur le thread pool-1-thread-5
Demande d'utilisateur traitée #6 sur le thread pool-1-thread-4
(4) java.util.concurrent.ThreadPoolExecutor@f6f4d33[En cours d'exécution, taille du pool = 2, threads actifs = 0, tâches en file d'attente = 0, tâches terminées = 7]

Passons en revue chacune des étapes :

Marcher Explication
1 (après 3 tâches terminées) Nous avons créé 3 threads, et 3 tâches ont été exécutées sur ces trois threads.
Lorsque l'état est affiché, les 3 tâches sont terminées et les threads sont prêts à effectuer d'autres tâches.
2 (après une pause de 30 secondes et l'exécution d'une autre tâche) Après 30 secondes d'inactivité, les threads sont toujours actifs et attendent des tâches.
Une autre tâche est ajoutée et exécutée sur un thread extrait du pool des threads actifs restants.
Aucun nouveau fil n'a été ajouté au pool.
3 (après une pause de 70 secondes) Les threads ont été supprimés du pool.
Il n'y a pas de threads prêts à accepter des tâches.
4 (après avoir exécuté 3 autres tâches) Après que plus de tâches aient été reçues, de nouveaux fils ont été créés. Cette fois, seuls deux threads ont réussi à traiter 3 tâches.

Eh bien, vous connaissez maintenant la logique d'un autre type de service d'exécuteur testamentaire.

Par analogie avec d'autres méthodes de la classe utilitaire Executors , la méthode newCachedThreadPool a également une version surchargée qui prend un objet ThreadFactory comme argument.