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 :
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.
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 Executors
newFixedThreadPool()
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 ExecutorService
execute(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 ExecutorService
submit(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 ExecutorService
submit()
:
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 ExecutorService
submit(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 ExecutorService
Callable
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 Runnable
run()
et la méthode Callable
call()
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
ExecutorService
en appelant la méthodecancel()
sur laFuture
retourné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 à ExecutorService
shutdownNow
:
executorService.shutdownNow();
awaitTermination()
La méthode ExecutorService
awaitTermination()
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 ExecutorService
awaitTermination()
:
executorService.shutdown();executorService.awaitTermination(10_000L, TimeUnit.MILLISECONDS );
.