Pourquoi avez-vous besoin de l'interface Executor ?

Avant Java 5, vous deviez écrire toute votre propre gestion des threads de code dans votre application. De plus, la création d'unnouveau filL'objet est une opération gourmande en ressources et cela n'a pas de sens de créer un nouveau thread pour chaque tâche légère. Et parce que ce problème est familier à absolument tous les développeurs d'applications multithreads, ils ont décidé d'intégrer cette fonctionnalité dans Java en tant que framework Executor .

Quelle est la grande idée ? C'est simple : au lieu de créer un nouveau thread pour chaque nouvelle tâche, les threads sont conservés dans une sorte de "stockage", et lorsqu'une nouvelle tâche arrive, on récupère un thread existant au lieu d'en créer un nouveau.

Les principales interfaces de ce framework sont Executor , ExecutorService et ScheduledExecutorService , chacune étendant les fonctionnalités de la précédente.

L'interface Executor est l'interface de base. Il déclare une seule méthode void execute(Runnable command) qui est implémentée par un objet Runnable .

L' interface ExecutorService est plus intéressante. Il a des méthodes pour gérer l'achèvement du travail, ainsi que des méthodes pour renvoyer une sorte de résultat. Examinons de plus près ses méthodes :

Méthode Description
annuler l'arrêt (); L'appel de cette méthode arrête ExecutorService . Toutes les tâches qui ont déjà été soumises pour traitement seront terminées, mais les nouvelles tâches ne seront pas acceptées.
List<Runnable> shutdownNow();

L'appel de cette méthode arrête ExecutorService . Thread.interrupt sera appelé pour toutes les tâches qui ont déjà été soumises pour traitement. Cette méthode renvoie une liste des tâches en file d'attente.

La méthode n'attend pas l'achèvement de toutes les tâches "en cours" au moment où la méthode est appelée.

Avertissement : L'appel de cette méthode peut entraîner une perte de ressources.

boolean isShutdown(); Vérifie si ExecutorService est arrêté.
booléen isTerminé(); Renvoie true si toutes les tâches ont été terminées après l'arrêt de ExecutorService . Jusqu'à ce que shutdown() ou shutdownNow() soit appelé, il renverra toujours false .
boolean awaitTermination(long timeout, unité TimeUnit) lance InterruptedException ;

Après l' appel de la méthode shutdown() , cette méthode bloque le thread sur lequel elle est appelée, jusqu'à ce que l'une des conditions suivantes soit vraie :

  • toutes les tâches planifiées sont terminées ;
  • le délai passé à la méthode est écoulé ;
  • le fil en cours est interrompu.

Renvoie true si toutes les tâches sont terminées et false si le délai d'attente s'est écoulé avant la fin.

<T> Future<T> submit(Callable<T> task);

Ajoute une tâche Callable à ExecutorService et retourne un objet qui implémente l' interface Future .

<T> est le type du résultat de la tâche passée.

<T> Future<T> submit(Tâche exécutable, résultat T);

Ajoute une tâche exécutable à ExecutorService et retourne un objet qui implémente l' interface Future .

Le paramètre de résultat T est ce qui est renvoyé par un appel à la méthode get () sur le résultatObjet futur.

Future<?> submit(Tâche exécutable) ;

Ajoute une tâche exécutable à ExecutorService et retourne un objet qui implémente l' interface Future .

Si nous appelons la méthode get() sur l' objet Future résultant , nous obtenons null.

<T> List<Future<T>> invokeAll(Collection<? étend les tâches Callable<T>>) lance InterruptedException ;

Passe une liste de tâches Callable à ExecutorService . Renvoie une liste de Futures à partir desquels nous pouvons obtenir le résultat du travail. Cette liste est renvoyée lorsque toutes les tâches soumises sont terminées.

Si la collection de tâches est modifiée pendant l'exécution de la méthode, le résultat de cette méthode est indéfini.

<T> List<Future<T>> invokeAll(Collection<? étend les tâches Callable<T>>, délai d'attente long, unité TimeUnit) lance InterruptedException ;

Passe une liste de tâches Callable à ExecutorService . Renvoie une liste de Futures à partir desquels nous pouvons obtenir le résultat du travail. Cette liste est renvoyée lorsque toutes les tâches transmises sont terminées ou après l'expiration du délai d'attente transmis à la méthode, selon la première éventualité.

Si le délai expire, les tâches non terminées sont annulées.

Remarque : Il est possible qu'une tâche annulée ne s'arrête pas (nous verrons cet effet secondaire dans l'exemple).

Si la collection de tâches est modifiée pendant l'exécution de la méthode, le résultat de cette méthode est indéfini.

<T> TaskAny(Collection<? étend les tâches Callable<T>>) lève InterruptedException, ExecutionException ;

Passe une liste de tâches Callable à ExecutorService . Renvoie le résultat de l'une des tâches (le cas échéant) qui s'est terminée sans lever d'exception (le cas échéant).

Si la collection de tâches est modifiée pendant l'exécution de la méthode, le résultat de cette méthode est indéfini.

<T> TaskAny(Collection<? étend les tâches Callable<T>>, délai d'attente long, unité TimeUnit) lève InterruptedException, ExecutionException, TimeoutException ;

Passe une liste de tâches Callable à ExecutorService . Renvoie le résultat de l'une des tâches (le cas échéant) qui s'est terminée sans lever d'exception avant que le délai d'attente passé à la méthode ne se soit écoulé.

Si la collection de tâches est modifiée pendant l'exécution de la méthode, le résultat de cette méthode est indéfini.

Regardons un petit exemple de travail avec ExecutorService .


import java.util.List;
import java.util.concurrent.*;

public class ExecutorServiceTest {
   public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
//Create an ExecutorService for 2 threads
       java.util.concurrent.ExecutorService executorService = new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
// Create 5 tasks
       MyRunnable task1 = new MyRunnable();
       MyRunnable task2 = new MyRunnable();
       MyRunnable task3 = new MyRunnable();
       MyRunnable task4 = new MyRunnable();
       MyRunnable task5 = new MyRunnable();

       final List<MyRunnable> tasks = List.of(task1, task2, task3, task4, task5);
// Pass a list that contains the 5 tasks we created
       final List<Future<Void>> futures = executorService.invokeAll(tasks, 6, TimeUnit.SECONDS);
       System.out.println("Futures received");

// Stop the ExecutorService
       executorService.shutdown();

       try {
           TimeUnit.SECONDS.sleep(3);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       System.out.println(executorService.isShutdown());
       System.out.println(executorService.isTerminated());
   }

   public static class MyRunnable implements Callable<Void> {

       @Override
       public void call() {
// Add 2 delays. When the ExecutorService is stopped, we will see which delay is in progress when the attempt is made to stop execution of the task
           try {
               TimeUnit.SECONDS.sleep(3);
           } catch (InterruptedException e) {
               System.out.println("sleep 1: " + e.getMessage());
           }
           try {
               TimeUnit.SECONDS.sleep(2);
           } catch (InterruptedException e) {
               System.out.println("sleep 2: " + e.getMessage());
           }
           System.out.println("done");
           return null;
       }
   }
}

Sortir:

done
done
Futures reçus
sleep 1 : sleep interrompu
sleep 1 : sleep interrompu
done
done
vrai
vrai

Chaque tâche s'exécute pendant 5 secondes. Nous avons créé un pool pour deux threads, donc les deux premières lignes de sortie sont parfaitement logiques.

Six secondes après le démarrage du programme, la méthode invokeAll expire et le résultat est renvoyé sous la forme d'une liste de Futures . Cela peut être vu à partir de la chaîne de sortie Futures receive .

Une fois les deux premières tâches terminées, deux autres commencent. Mais comme le délai d'attente défini dans la méthode invokeAll expire, ces deux tâches n'ont pas le temps de se terminer. Ils reçoivent une commande "annuler" . C'est pourquoi la sortie comporte deux lignes avec sleep 1 : sleep interruption .

Et puis vous pouvez voir deux autres lignes avec done . C'est l'effet secondaire que j'ai mentionné lors de la description de la méthode invokeAll .

La cinquième et dernière tâche ne démarre même jamais, nous ne voyons donc rien à ce sujet dans la sortie.

Les deux dernières lignes sont le résultat de l'appel des méthodes isShutdown et isTerminated .

Il est également intéressant d'exécuter cet exemple en mode débogage et de regarder l'état de la tâche une fois le délai écoulé (définissez un point d'arrêt sur la ligne avec executorService.shutdown(); ) :

Nous voyons que deux tâches se sont terminées normalement et que trois tâches ont été "annulées" .

ScheduledExecutorService

Pour conclure notre discussion sur les exécuteurs, examinons ScheduledExecutorService .

Il dispose de 4 méthodes :

Méthode Description
public ScheduledFuture<?> schedule (Commande exécutable, long délai, unité TimeUnit); Planifie la tâche exécutable transmise pour qu'elle s'exécute une fois après le délai spécifié en tant qu'argument.
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit); Planifie la tâche appelable transmise pour qu'elle s'exécute une fois après le délai spécifié en tant qu'argument.
public ScheduledFuture<?> scheduleAtFixedRate(Commande exécutable, long initialDelay, longue période, unité TimeUnit); Planifie l'exécution périodique de la tâche passée, qui sera exécutée pour la première fois après initialDelay , et chaque exécution suivante commencera après period .
public ScheduledFuture<?> scheduleWithFixedDelay(Commande exécutable, long initialDelay, long delay, unité TimeUnit); Planifie l'exécution périodique de la tâche passée, qui sera exécutée pour la première fois après initialDelay , et chaque exécution suivante commencera après un délai (la période entre la fin de l'exécution précédente et le début de celle en cours).