Un guide sur le Java ExecutorService

Un guide sur le Java ExecutorService

1. Vue d'ensemble

ExecutorService est un framework fourni par le JDK qui simplifie l'exécution des tâches en mode asynchrone. De manière générale,ExecutorService fournit automatiquement un pool de threads et d'API pour lui attribuer des tâches.

Lectures complémentaires:

Guide du framework Fork / Join en Java

Introduction au framework fork / join présenté dans Java 7 et aux outils permettant d’accélérer le traitement parallèle en tentant d’utiliser tous les cœurs de processeur disponibles.

Read more

Vue d'ensemble de java.util.concurrent

Découvrez le contenu du package java.util.concurrent.

Read more

Guide de java.util.concurrent.Locks

Dans cet article, nous explorons diverses implémentations de l'interface Lock et de la nouvelle classe introduite dans Java 9 StampedLock.

Read more

2. Instanciation deExecutorService

2.1. Méthodes d'usine de la classeExecutors

Le moyen le plus simple de créer desExecutorService est d'utiliser l'une des méthodes d'usine de la classeExecutors.

Par exemple, la ligne de code suivante créera un pool de threads avec 10 threads:

ExecutorService executor = Executors.newFixedThreadPool(10);

Il existe plusieurs autres méthodes d'usine pour créer desExecutorServiceprédéfinis qui répondent à des cas d'utilisation spécifiques. Pour trouver la meilleure méthode pour vos besoins, consultezOracle’s official documentation.

2.2. Créer directement unExecutorService

CommeExecutorService est une interface, une instance de ses implémentations peut être utilisée. Vous avez le choix entre plusieurs implémentations dans le packagejava.util.concurrent ou vous pouvez créer la vôtre.

Par exemple, la classeThreadPoolExecutor a quelques constructeurs qui peuvent être utilisés pour configurer un service exécuteur et son pool interne.

ExecutorService executorService =
  new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
  new LinkedBlockingQueue());

Vous remarquerez peut-être que le code ci-dessus est très similaire auxsource code de la méthode d'usinenewSingleThreadExecutor().. Dans la plupart des cas, une configuration manuelle détaillée n'est pas nécessaire.

3. Attribution de tâches auxExecutorService

ExecutorService peut exécuter les tâchesRunnable etCallable. Pour que les choses restent simples dans cet article, deux tâches primitives seront utilisées. Notez que les expressions lambda sont utilisées ici au lieu des classes internes anonymes:

Runnable runnableTask = () -> {
    try {
        TimeUnit.MILLISECONDS.sleep(300);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

Callable callableTask = () -> {
    TimeUnit.MILLISECONDS.sleep(300);
    return "Task's execution";
};

List> callableTasks = new ArrayList<>();
callableTasks.add(callableTask);
callableTasks.add(callableTask);
callableTasks.add(callableTask);

Les tâches peuvent être attribuées auxExecutorService en utilisant plusieurs méthodes, y comprisexecute(), qui est hérité de l'interfaceExecutor, et aussisubmit(),invokeAny(), invokeAll().

La méthodeexecute() estvoid, et elle ne donne aucune possibilité d’obtenir le résultat de l’exécution de la tâche ou de vérifier l’état de la tâche (est-elle en cours d’exécution ou exécutée).

executorService.execute(runnableTask);

submit() soumet une tâcheCallable ouRunnable à unExecutorService et renvoie un résultat de typeFuture.

Future future =
  executorService.submit(callableTask);

invokeAny() assigne une collection de tâches à unExecutorService, provoquant l'exécution de chacune d'entre elles, et renvoie le résultat d'une exécution réussie d'une tâche (s'il y a eu une exécution réussie).

String result = executorService.invokeAny(callableTasks);

invokeAll() assigne une collection de tâches à unExecutorService, provoquant l'exécution de chacune, et retourne le résultat de toutes les exécutions de tâches sous la forme d'une liste d'objets de typeFuture.

List> futures = executorService.invokeAll(callableTasks);

Maintenant, avant d'aller plus loin, deux autres choses doivent être discutées: arrêter unExecutorService et traiter les types de retourFuture.

4. Arrêt d'unExecutorService

En général, lesExecutorService ne seront pas automatiquement détruits lorsqu'il n'y a pas de tâche à traiter. Il restera en vie et attendra un nouveau travail à faire.

Dans certains cas, cela est très utile. Par exemple, si une application doit traiter des tâches qui apparaissent de manière irrégulière ou si leur quantité n'est pas connue au moment de la compilation.

D'un autre côté, une application pourrait atteindre sa fin, mais elle ne sera pas arrêtée car une attenteExecutorService entraînera la poursuite de l'exécution de la JVM.

Pour arrêter correctement unExecutorService, nous avons les APIshutdown() etshutdownNow().

La méthodeshutdown() ne provoque pas de destruction immédiate desExecutorService.. Elle fera que lesExecutorService cesseront d'accepter de nouvelles tâches et s'arrêteront une fois que tous les threads en cours auront terminé leur travail actuel.

executorService.shutdown();

La méthodeshutdownNow() essaie de détruire lesExecutorService immédiatement, mais elle ne garantit pas que tous les threads en cours seront arrêtés en même temps. Cette méthode retourne une liste des tâches en attente de traitement. Il appartient au développeur de décider quoi faire avec ces tâches.

List notExecutedTasks = executorService.shutDownNow();

Une bonne façon d'arrêter leExecutorService (qui est aussirecommended by Oracle) est d'utiliser ces deux méthodes combinées avec la méthodeawaitTermination(). Avec cette approche, lesExecutorService arrêteront d'abord de prendre de nouvelles tâches, en attendant jusqu'à une période de temps spécifiée pour que toutes les tâches soient terminées. Si ce délai expire, l'exécution est immédiatement arrêtée:

executorService.shutdown();
try {
    if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
        executorService.shutdownNow();
    }
} catch (InterruptedException e) {
    executorService.shutdownNow();
}

5. L'interfaceFuture

Les méthodessubmit() etinvokeAll() retournent un objet ou une collection d'objets de typeFuture, ce qui nous permet d'obtenir le résultat de l'exécution d'une tâche ou de vérifier l'état de la tâche (est-elle en cours ou exécuté).

L'interfaceFuture fournit une méthode de blocage spécialeget() qui renvoie un résultat réel de l'exécution de la tâcheCallable ounull dans le cas de la tâcheRunnable. L'appel de la méthodeget() alors que la tâche est toujours en cours d'exécution bloquera l'exécution jusqu'à ce que la tâche soit correctement exécutée et que le résultat soit disponible.

Future future = executorService.submit(callableTask);
String result = null;
try {
    result = future.get();
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

Avec un blocage très long causé par la méthodeget(), les performances d’une application peuvent se dégrader. Si les données résultantes ne sont pas cruciales, il est possible d'éviter un tel problème en utilisant des délais d'attente:

String result = future.get(200, TimeUnit.MILLISECONDS);

Si la période d'exécution est plus longue que celle spécifiée (dans ce cas 200 millisecondes), unTimeoutException sera levé.

La méthodeisDone() peut être utilisée pour vérifier si la tâche affectée est déjà traitée ou non.

L'interfaceFuture prévoit également l'annulation de l'exécution de la tâche avec la méthodecancel(), et de vérifier l'annulation avec la méthodeisCancelled():

boolean canceled = future.cancel(true);
boolean isCancelled = future.isCancelled();

6. L'interfaceScheduledExecutorService

LeScheduledExecutorService exécute des tâches après un délai prédéfini et / ou périodiquement. Encore une fois, la meilleure façon d'instancier unScheduledExecutorService est d'utiliser les méthodes de fabrique de la classeExecutors.

Pour cette section, unScheduledExecutorService avec un thread sera utilisé:

ScheduledExecutorService executorService = Executors
  .newSingleThreadScheduledExecutor();

Pour planifier l'exécution d'une seule tâche après un délai fixe, utilisez la méthodescheduled() desScheduledExecutorService. Il existe deux méthodesscheduled() qui vous permettent d'exécuter les tâchesRunnable ouCallable:

Future resultFuture =
  executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

La méthodescheduleAtFixedRate() permet d'exécuter une tâche périodiquement après un délai fixe. Le code ci-dessus retarde une seconde avant d'exécutercallableTask.

Le bloc de code suivant exécutera une tâche après un délai initial de 100 millisecondes, après quoi il exécutera la même tâche toutes les 450 millisecondes. Si le processeur a besoin de plus de temps pour exécuter une tâche assignée que le paramètreperiod de la méthodescheduleAtFixedRate(), lesScheduledExecutorService attendront que la tâche en cours soit terminée avant de commencer la suivante:

Future resultFuture = service
  .scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

S'il est nécessaire d'avoir un délai de longueur fixe entre les itérations de la tâche,scheduleWithFixedDelay() doit être utilisé. Par exemple, le code suivant garantira une pause de 150 millisecondes entre la fin de l'exécution en cours et le début d'une autre.

service.scheduleWithFixedDelay(task, 100, 150, TimeUnit.MILLISECONDS);

Selon les contrats de méthodescheduleAtFixedRate() etscheduleWithFixedDelay(), l'exécution de la période de la tâche se terminera à la fin desExecutorService ou si une exception est levée lors de l'exécution de la tâche.

7. ExecutorService contre Fork/Join

Après la sortie de Java 7, de nombreux développeurs ont décidé que le frameworkExecutorService devrait être remplacé par le framework fork / join. Ce n'est pas toujours la bonne décision, cependant. Malgré la simplicité d'utilisation et les gains fréquents de performances associés au fork / join, le contrôle exercé par le développeur sur l'exécution simultanée est également réduit.

ExecutorService donne au développeur la possibilité de contrôler le nombre de threads générés et la granularité des tâches qui doivent être exécutées par des threads séparés. Le meilleur cas d'utilisation deExecutorService est le traitement de tâches indépendantes, telles que des transactions ou des demandes, selon le schéma «un thread pour une tâche».

En revanche,according to Oracle’s documentation, fork / join a été conçu pour accélérer le travail qui peut être divisé en petits morceaux de manière récursive.

8. Conclusion

Malgré la relative simplicité deExecutorService, il existe quelques pièges courants. Résumons-les:

Keeping an unused ExecutorService alive: Il y a une explication détaillée dans la section 4 de cet article sur la façon d'arrêter unExecutorService;

Wrong thread-pool capacity while using fixed length thread-pool: Il est très important de déterminer le nombre de threads dont l'application aura besoin pour exécuter efficacement les tâches. Un pool de threads trop volumineux entraînera une surcharge inutile simplement pour créer des threads qui seront pour la plupart en mode attente. Trop peu de personnes peuvent faire en sorte qu'une application semble ne pas répondre à cause des longues périodes d'attente pour les tâches de la file d'attente.

Calling a Future‘s get() method after task cancellation: Une tentative d'obtenir le résultat d'une tâche déjà annulée déclenchera unCancellationException.

Les délais d'expiration deUnexpectedly-long blocking with Future‘s get() method: doivent être utilisés pour éviter les attentes inattendues.

Le code de cet article est disponible ena GitHub repository.