L’interface Java ExecutorService, java.util.concurrent.ExecutorService, représente un mécanisme d’exécution asynchrone qui est capable d’exécuter des tâches simultanément en arrière-plan. Dans ce tutoriel Java ExecutorService, j’expliquerai comment créer un ExecutorService, comment lui soumettre des tâches à exécuter, comment voir les résultats de ces tâches et comment arrêter à nouveau le ExecutorService lorsque vous en avez besoin.

Tutoriel vidéo de Java ExecutorService

Si vous préférez la vidéo, j’ai une introduction vidéo au ici :

Vidéo de tutorat de Java ExecutorService - Partie 1
Vidéo de tutorat de Java ExecutorService - Partie 2

Délégation de tâches

Voici un schéma illustrant un thread déléguant une tâche à un ExecutorService Java pour une exécution asynchrone :

Un thread déléguant une tâche à un ExecutorService pour une exécution asynchrone.

Un thread déléguant une tâche à un ExecutorService pour une exécution asynchrone.

Une fois que le thread a délégué la tâche à l’ExecutorService, le thread poursuit sa propre exécution indépendamment de l’exécution de cette tâche. Le ExecutorService exécute alors la tâche de manière concurrente, indépendamment du thread qui a soumis la tâche.

Exemple d’ExecutorService Java

Avant de nous plonger trop profondément dans le ExecutorService, examinons un exemple simple. Voici un exemple simple en Java ExecutorService:

ExecutorService executorService = Executors.newFixedThreadPool(10);executorService.execute(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});executorService.shutdown();

D’abord, une ExecutorService est créée à l’aide de la méthode ExecutorsnewFixedThreadPool() factory. Cela crée un pool de threads avec 10 threads exécutant des tâches.

Deuxièmement, une implémentation anonyme de l’interface Runnable est passée à la méthode execute(). Cela entraîne l’exécution de la Runnable par l’un des threads de la ExecutorService.

Vous verrez plusieurs autres exemples d’utilisation de la ExecutorService tout au long de ce tutoriel. Cet exemple a juste servi à vous donner un aperçu rapide de ce à quoi ressemble l’utilisation d’un ExecutorService pour exécuter des tâches en arrière-plan.

Mise en œuvre de ExecutorService Java

Le ExecutorService Java est très similaire à un pool de threads. En fait, l’implémentation de l’interface ExecutorService présente dans le paquet java.util.concurrent est une implémentation de pool de threads. Si vous voulez comprendre comment l’interface ExecutorService peut être implémentée en interne, lisez le tutoriel ci-dessus.

Comme ExecutorService est une interface, vous avez besoin de ses implémentations pour en faire un usage quelconque. Le ExecutorService possède l’implémentation suivante dans le paquet java.util.concurrent :

  • ThreadPoolExecutor
  • ScheduledThreadPoolExecutor

Création d’un ExecutorService

La façon dont vous créez un ExecutorService dépend de l’implémentation que vous utilisez. Cependant, vous pouvez utiliser la classe de fabrique Executors pour créer des instances ExecutorService également. Voici quelques exemples de création d’un ExecutorService:

ExecutorService executorService1 = Executors.newSingleThreadExecutor();ExecutorService executorService2 = Executors.newFixedThreadPool(10);ExecutorService executorService3 = Executors.newScheduledThreadPool(10);

Utilisation de l’ExecutorService

Il existe plusieurs façons différentes de déléguer des tâches à exécuter à un ExecutorService :

  • exécuter(Runnable)
  • soumettre(Runnable)
  • soumettre(Callable)
  • invokeAny(….)
  • invokeAll(…)

Je vais examiner chacune de ces méthodes dans les sections suivantes.

Exécuter un exécutable

La méthode Java ExecutorServiceexecute(Runnable) prend un objet java.lang.Runnable, et l’exécute de manière asynchrone. Voici un exemple d’exécution d’une Runnable avec une ExecutorService:

ExecutorService executorService = Executors.newSingleThreadExecutor();executorService.execute(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});executorService.shutdown();

Il n’y a aucun moyen d’obtenir le résultat de la Runnable exécutée, si nécessaire. Vous devrez utiliser une Callable pour cela (expliqué dans les sections suivantes).

Soumettre un exécutable

La méthode Java ExecutorServicesubmit(Runnable) prend également une implémentation Runnable, mais renvoie un objet Future. Cet objet Future peut être utilisé pour vérifier si la Runnable a fini de s’exécuter.

Voici un exemple en Java ExecutorServicesubmit() :

Future future = executorService.submit(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});future.get(); //returns null if the task has finished correctly.

La méthode submit() renvoie un objet Java Future qui peut être utilisé pour vérifier quand la Runnable est terminée.

Soumettre un appelant

La méthode Java ExecutorServicesubmit(Callable) est similaire à la méthode submit(Runnable) sauf qu’elle prend un appelant Java au lieu d’un Runnable. La différence précise entre un Callable et un Runnable est expliquée un peu plus loin.

Le résultat du Callable peut être obtenu via l’objet Java Future renvoyé par la méthode submit(Callable). Voici un ExecutorServiceCallable exemple:

Future future = executorService.submit(new Callable(){ public Object call() throws Exception { System.out.println("Asynchronous Callable"); return "Callable Result"; }});System.out.println("future.get() = " + future.get());

L’exemple de code ci-dessus donnera le résultat suivant :

Asynchronous Callablefuture.get() = Callable Result

invokeAny()

La méthode invokeAny() prend une collection d’objets Callable, ou de sous-interfaces de Callable. L’invocation de cette méthode ne renvoie pas un Future, mais renvoie le résultat de l’un des objets Callable. Vous n’avez aucune garantie sur lequel des résultats de Callable vous obtenez. Juste l’un de ceux qui se terminent.

Si un Callable se termine, de sorte qu’un résultat est renvoyé par invokeAny(), alors le reste des instances Callable est annulé.

Si l’une des tâches se termine (ou lève une exception), le reste des Callable est annulé.

Voici un exemple de code:

ExecutorService executorService = Executors.newSingleThreadExecutor();Set<Callable<String>> callables = new HashSet<Callable<String>>();callables.add(new Callable<String>() { public String call() throws Exception { return "Task 1"; }});callables.add(new Callable<String>() { public String call() throws Exception { return "Task 2"; }});callables.add(new Callable<String>() { public String call() throws Exception { return "Task 3"; }});String result = executorService.invokeAny(callables);System.out.println("result = " + result);executorService.shutdown();

Cet exemple de code va imprimer l’objet renvoyé par l’un des Callable de la collection donnée. J’ai essayé de l’exécuter plusieurs fois, et le résultat change. Parfois c’est « Tâche 1 », parfois « Tâche 2 » etc.

invokeAll()

La méthode invokeAll() invoque tous les objets Callable que vous lui passez dans la collection passée en paramètre. La invokeAll() renvoie une liste d’objets Future via laquelle vous pouvez obtenir les résultats des exécutions de chaque Callable.

N’oubliez pas qu’une tâche peut se terminer à cause d’une exception, elle n’a donc pas forcément « réussi ». Il n’y a aucun moyen sur un Future de faire la différence.

Voici un exemple de code:

ExecutorService executorService = Executors.newSingleThreadExecutor();Set<Callable<String>> callables = new HashSet<Callable<String>>();callables.add(new Callable<String>() { public String call() throws Exception { return "Task 1"; }});callables.add(new Callable<String>() { public String call() throws Exception { return "Task 2"; }});callables.add(new Callable<String>() { public String call() throws Exception { return "Task 3"; }});List<Future<String>> futures = executorService.invokeAll(callables);for(Future<String> future : futures){ System.out.println("future.get = " + future.get());}executorService.shutdown();

Runnable vs. Callable

L’interface Runnable est très similaire à l’interface Callable. L’interface Runnable représente une tâche qui peut être exécutée simultanément par un thread ou un ExecutorService. L’interface Callable ne peut être exécutée que par un ExecutorService. Les deux interfaces ne possèdent qu’une seule méthode. Il existe cependant une petite différence entre l’interface Callable et Runnable. La différence entre l’interface Runnable et Callable est plus facilement visible lorsque vous voyez les déclarations d’interface.

Voici d’abord la déclaration d’interface Runnable:

public interface Runnable { public void run();}

Et voici la déclaration d’interface Callable :

public interface Callable{ public Object call() throws Exception;}

La principale différence entre la méthode Runnablerun() et la méthode Callablecall() est que la méthode call() peut retourner une Object à partir de l’appel de la méthode. Une autre différence entre call() et run() est que call() peut lancer une exception, alors que run() ne le peut pas (sauf pour les exceptions non vérifiées – sous-classes de RuntimeException).

Si vous devez soumettre une tâche à une ExecutorService Java et que vous avez besoin d’un résultat de la tâche, alors vous devez faire en sorte que votre tâche implémente l’interface Callable. Sinon, votre tâche peut simplement mettre en œuvre l’interface Runnable.

Annulation de la tâche

Vous pouvez annuler une tâche (Runnable ou Callable) soumise à une

Java.ExecutorService

en appelant la méthodecancel()sur laFutureretournée lorsque la tâche est soumise. L’annulation de la tâche n’est possible que si la tâche n’a pas encore commencé à s’exécuter. Voici un exemple d’annulation d’une tâche en appelant la méthodeFuture.cancel():

future.cancel();

ExecutorService Shutdown

Lorsque vous avez fini d’utiliser le ExecutorService Java, vous devez l’arrêter, afin que les threads ne continuent pas à tourner. Si votre application est lancée via une méthode main() et que votre thread principal quitte votre application, celle-ci continuera à fonctionner si vous avez une ExexutorService active dans votre application. Les threads actifs à l’intérieur de cette ExecutorService empêchent la JVM de s’arrêter.

shutdown()

Pour mettre fin aux threads à l’intérieur de la ExecutorService, vous appelez sa méthode shutdown(). La ExecutorService ne s’arrête pas immédiatement, mais elle n’accepte plus de nouvelles tâches, et une fois que tous les threads ont terminé les tâches en cours, la ExecutorService s’arrête. Toutes les tâches soumises à la ExecutorService avant que shutdown() ne soit appelée, sont exécutées. Voici un exemple d’exécution d’un arrêt de Java ExecutorService :

executorService.shutdown();

shutdownNow()

Si vous voulez arrêter la ExecutorService immédiatement, vous pouvez appeler la méthode shutdownNow(). Cela tentera d’arrêter toutes les tâches en cours d’exécution immédiatement, et ignore toutes les tâches soumises mais non traitées. Aucune garantie n’est donnée concernant les tâches en cours d’exécution. Peut-être s’arrêteront-elles, peut-être s’exécuteront-elles jusqu’à la fin. Il s’agit d’une tentative de meilleur effort. Voici un exemple d’appel à ExecutorServiceshutdownNow :

executorService.shutdownNow();

awaitTermination()

La méthode ExecutorServiceawaitTermination() bloquera le thread qui l’appelle jusqu’à ce que soit le ExecutorService se soit arrêté complètement, ou jusqu’à ce qu’un temps mort donné se produise. La méthode awaitTermination() est généralement appelée après avoir appelé shutdown() ou shutdownNow(). Voici un exemple d’appel de ExecutorServiceawaitTermination():

executorService.shutdown();executorService.awaitTermination(10_000L, TimeUnit.MILLISECONDS );

.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *