La méthode newFixedThreadPool de la classe Executors crée un executorService avec un nombre fixe de threads. Contrairement à la méthode newSingleThreadExecutor , nous spécifions le nombre de threads que nous voulons dans le pool. Sous le capot, le code suivant s'appelle :


new ThreadPoolExecutor(nThreads, nThreads,
                                      	0L, TimeUnit.MILLISECONDS,
                                      	new LinkedBlockingQueue());

Les paramètres corePoolSize (le nombre de threads qui seront prêts (démarrés) au démarrage du service exécuteur ) et maximumPoolSize (le nombre maximum de threads que le service exécuteur peut créer) reçoivent la même valeur — le nombre de threads passés à newFixedThreadPool(nThreads ) . Et nous pouvons passer notre propre implémentation de ThreadFactory exactement de la même manière.

Eh bien, voyons pourquoi nous avons besoin d'un tel ExecutorService .

Voici la logique d'un ExecutorService avec un nombre fixe (n) de threads :

  • Un maximum de n threads seront actifs pour le traitement des tâches.
  • Si plus de n tâches sont soumises, elles seront maintenues dans la file d'attente jusqu'à ce que les threads se libèrent.
  • Si l'un des threads échoue et se termine, un nouveau thread sera créé pour le remplacer.
  • Tout thread du pool est actif jusqu'à ce que le pool soit arrêté.

Par exemple, imaginez que vous attendiez pour passer le contrôle de sécurité à l'aéroport. Tout le monde fait la queue jusqu'à ce qu'immédiatement avant le contrôle de sécurité, les passagers soient répartis entre tous les points de contrôle en activité. S'il y a un retard à l'un des points de contrôle, la file d'attente sera traitée uniquement par le second jusqu'à ce que le premier soit libre. Et si un point de contrôle ferme complètement, un autre point de contrôle sera ouvert pour le remplacer, et les passagers continueront à être traités par deux points de contrôle.

On notera tout de suite que même si les conditions sont idéales — les n threads promis fonctionnent de manière stable, et les threads qui se terminent par une erreur sont toujours remplacés (ce que des ressources limitées rendent impossible à réaliser dans un véritable aéroport) — le système a encore plusieurs fonctionnalités désagréables, car en aucun cas il n'y aura plus de threads, même si la file d'attente augmente plus vite que les threads ne peuvent traiter les tâches.

Je suggère d'acquérir une compréhension pratique du fonctionnement d'ExecutorService avec un nombre fixe de threads. Créons une classe qui implémente Runnable . Les objets de cette classe représentent nos tâches pour ExecutorService .


public class Task implements Runnable {
    int taskNumber;
 
    public Task(int taskNumber) {
        this.taskNumber = taskNumber;
    }
 
    @Override
    public void run() {
try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
    }
}
    

Dans la méthode run() , nous bloquons le thread pendant 2 secondes, simulant une certaine charge de travail, puis affichons le numéro de la tâche en cours et le nom du thread exécutant la tâche.


ExecutorService executorService = Executors.newFixedThreadPool(3);
 
        for (int i = 0; i < 30; i++) {
            executorService.execute(new Task(i));
        }
        
        executorService.shutdown();
    

Pour commencer, dans la méthode principale , nous créons un ExecutorService et soumettons 30 tâches pour exécution.

Demande utilisateur traitée n°1 sur le thread pool-1-thread-2
Demande utilisateur traitée n°0 sur le thread pool-1-thread-1
Demande utilisateur traitée n°2 sur le thread pool-1-thread-3 Demande
utilisateur traitée n°5 sur pool- 1-thread-3 thread
Demande utilisateur traitée n°3 sur pool-1-thread-2 thread
Demande utilisateur traitée n°4 sur pool-1-thread-1 thread
Demande utilisateur traitée n°8 sur pool-1-thread-1 thread
Utilisateur traité requête n° 6 sur le thread pool-1-thread-3
Requête utilisateur n° 7 traitée sur le thread pool-1-thread-2
Requête utilisateur n° 10 traitée sur le thread pool-1-thread-3
Requête utilisateur n° 9 traitée sur pool-1- thread-1 thread
Demande utilisateur traitée n° 11 sur le thread pool-1-thread-2
Demande utilisateur traitée n° 12 sur le thread pool-1-thread-3
Demande utilisateur traitée n° 14 sur le thread pool-1-thread-2
Demande utilisateur traitée n° 13 sur le thread pool-1-thread-1
Demande utilisateur traitée n° 15 sur le thread pool-1-thread-3 Demande
utilisateur traitée n° 16 sur pool- 1-thread-2 thread
Demande utilisateur traitée n° 17 sur pool-1-thread-1 thread
Demande utilisateur traitée n° 18 sur pool-1-thread-3 thread
Demande utilisateur traitée n° 19 sur pool-1-thread-2 thread
Utilisateur traité Requête n°20 sur le thread pool-1-thread-1
Requête utilisateur traitée n°21 sur le thread pool-1-thread-3
Requête utilisateur traitée n°22 sur le thread pool-1-thread-2
Requête utilisateur traitée n°23 sur le pool-1- thread-1 thread
Demande utilisateur traitée n° 25 sur le thread pool-1-thread-2
Demande utilisateur traitée n° 24 sur le thread pool-1-thread-3
Demande utilisateur traitée n° 26 sur le thread pool-1-thread-1
Demande utilisateur traitée n° 27 sur le thread pool-1-thread-2
Demande utilisateur traitée n° 28 sur le thread pool-1-thread-3 Demande
utilisateur traitée n° 29 sur pool- 1-fil-1 fil

La sortie de la console nous montre comment les tâches sont exécutées sur différents threads une fois qu'elles sont libérées par la tâche précédente.

Nous allons maintenant augmenter le nombre de tâches à 100, et après avoir soumis 100 tâches, nous appellerons la méthode awaitTermination(11, SECONDS) . Nous passons un nombre et une unité de temps comme arguments. Cette méthode bloquera le thread principal pendant 11 secondes. Ensuite, nous appellerons shutdownNow() pour forcer ExecutorService à s'arrêter sans attendre que toutes les tâches soient terminées.


ExecutorService executorService = Executors.newFixedThreadPool(3);
 
        for (int i = 0; i < 100; i++) {
            executorService.execute(new Task(i));
        }
 
        executorService.awaitTermination(11, SECONDS);
 
        executorService.shutdownNow();
        System.out.println(executorService);
    

À la fin, nous afficherons des informations sur l'état de executorService .

Voici la sortie de la console que nous obtenons :

Demande utilisateur traitée n° 0 sur le thread pool-1-thread-1
Demande utilisateur traitée n° 2 sur le thread pool-1-thread-3
Demande utilisateur traitée n° 1 sur le thread pool-1-thread-2
Demande utilisateur traitée n° 4 sur pool- 1-thread-3 thread
Demande utilisateur traitée n°5 sur pool-1-thread-2 thread
Demande utilisateur traitée n°3 sur pool-1-thread-1 thread
Demande utilisateur traitée n°6 sur pool-1-thread-3 thread
Utilisateur traité requête n° 7 sur le thread pool-1-thread-2
requête utilisateur traitée n° 8 sur le thread pool-1-thread-1
requête utilisateur traitée n° 9 sur le thread pool-1-thread-3
requête utilisateur traitée n° 11 sur pool-1- thread-1 thread
Demande utilisateur traitée n° 10 sur le thread pool-1-thread-2
Demande utilisateur traitée n° 13 sur le thread pool-1-thread-1
Demande utilisateur traitée #14 sur le thread pool-1-thread-2
Demande utilisateur traitée #12 sur le thread pool-1-thread-3
java.util.concurrent.ThreadPoolExecutor@452b3a41[Arrêt, taille du pool = 3, threads actifs = 3 , tâches en file d'attente = 0, tâches terminées = 15]
Demande utilisateur traitée n° 17 sur le thread pool-1-thread-3
Demande utilisateur traitée n° 15 sur le thread pool-1-thread-1
Demande utilisateur traitée n° 16 sur le pool-1-thread -2 fils

Ceci est suivi de 3 InterruptedExceptions , lancées par les méthodes sleep à partir de 3 tâches actives.

Nous pouvons voir que lorsque le programme se termine, 15 tâches sont effectuées, mais le pool avait encore 3 threads actifs qui n'ont pas fini d'exécuter leurs tâches. La méthode interrupt() est appelée sur ces trois threads, ce qui signifie que la tâche se terminera, mais dans notre cas, la méthode sleep lève une InterruptedException . Nous voyons également qu'après l'appel de la méthode shutdownNow() , la file d'attente des tâches est effacée.

Ainsi, lorsque vous utilisez un ExecutorService avec un nombre fixe de threads dans le pool, assurez-vous de vous souvenir de son fonctionnement. Ce type convient aux tâches avec une charge constante connue.

Voici une autre question intéressante : si vous avez besoin d'utiliser un exécuteur pour un seul thread, quelle méthode devez-vous appeler ? newSingleThreadExecutor() ou newFixedThreadPool(1) ?

Les deux exécuteurs auront un comportement équivalent. La seule différence est que la méthode newSingleThreadExecutor() renverra un exécuteur qui ne pourra pas être reconfiguré ultérieurement pour utiliser des threads supplémentaires.