On cherche à effectuer en parallèle des tâches indépendantes (requêtes BdD, requêtes Web, traitement d'images, ...).
Problèmes :
thread.start()
est lente pour les threads plateform,Solution : réutiliser les threads pour des tâches qui ont une valeur de retour.
Callable<T>
:
@FunctionalInterface public interface Callable<T> { T call() throws Exception; }
Future<T>
est renvoyé au moment
de la soumission de la tâche. Il permettra d'obtenir le résultat (de type T
).ExecutorService
On construit généralement les ExecutorService avec les méthodes factory de la classe Executors
:
Executors.newFixedThreadPool(int poolSize)
démarre poolSize
worker-threads qui ne sont jamais arrêtés.Executors.newCachedThreadPool()
démarre autant de worker-threads que nécessaire, en essayant de réutiliser les threads déjà démarrés. Les worker-threads inactifs depuis 1 minute sont arrêtés.Executors.newSingleThreadPool()
démarre un seul thread qui n'est jamais arrêté.Dans ces 3 cas, la BlockingQueue
qui stocke les tâches est non-bornée.
ThreadPoolExecutor
Le constructeur de ThreadPoolExecutor
prend 5 paramètres:
int corePoolSize
: nombre de threads qu'on s'attend à devoir utiliser en régime de croisière.
int maximumPoolSize
: le nombre maximum de threads vivants.
long keepAliveTime
: si il y a plus de corePoolSize
threads vivants, ceux en trop sont terminés au bout de keepAliveTime
unités de temps d'inactivité.
TimeUnit unit
: l'unité de temps d'inactivité.
BlockingQueue<Runnable> workQueue
: la file pour stocker les tâches si tous les worker-threads travaillent.
On peut ajouter une tâche à effectuer avec la méthode submit
.
<T> Future<T> submit(Callable<T> callable) // ou Future<?> submit(Runnable run) // le Future n'a pas de type car un Runnable ne renvoie rien.
Cette méthode n'est pas bloquante. Elle renvoie directement un objet de la classe Future<T>
qui permettra de récupérer la valeur renvoyée par le Callable<T>
.
Permet de demander un résultat et de contrôler l'exécution de la tâche.
future.get()
bloque jusqu'à ce que le Callable
correspondant ait été exécuté et renvoie la valeur calculée par le Callable
.Callable
lève une exception, l'appel à future.get()
lèvera l'exception ExecutionException
.future.resultNow()
renvoie la valeur à condition que le calcul soit fini.state()
permet d'avoir l'état de la tâche (RUNNING, SUCCESS, CANCELED, FAILED)RUNNING
: en cours d'exécution ou dans la file,SUCCESS
: tâche terminée sans avoir levé d'exception,FAILED
: tâche terminée par une exception,CANCELLED
: tâche annulée par la méthode cancel
.cancel(boolean interrupt)
permet de demander l'arrêt de l'exécution d'une tâchepublic static int bigComputation(int j) { // Syracuse var i = 0; while (j > 1) { j = j % 2 == 0 ? j = j / 2 : 3 * j + 1; i++; } return i; } public static void main(String[] args) throws InterruptedException { var executorService = Executors.newFixedThreadPool(2); var future = executorService.submit(() -> bigComputation(3333)); try { System.out.println(future.get()); } catch (ExecutionException e) { throw new AssertionError(e.getCause()); } ... }
invokeAll()
et invokeAny()
En pratique, on utilise souvent l'une des deux méthodes suivantes d'un ExecutorService
:
invokeAll
qui prend une collection de Callable<T>
, bloque jusqu'à ce qu'ils aient tous été exécutés (quel que soit le résultat de l’exécution). Elle renvoie la liste des Future<T>
associées.
invokeAny
qui prend une collection de Callable<T>
, bloque jusqu'à ce que l'un d'entre eux ait été exécuté (sans lever d'exception) et renvoie la valeur renvoyée par le Callable<T>
(ici, on n'a pas besoin d'un Future<T>
pour gérer l'exception éventuelle, puisque l'on sait que le Callable
s'est terminé correctement).var executorService = Executors.newFixedThreadPool(2); var callables = new ArrayList<Callable<Integer>>(); IntStream.range(1, 100).forEach(i -> callables.add(() -> bigComputation(i))); var futures = executorService.invokeAll(callables); try { for (var future : futures){ System.out.println(future.get()); } } catch (ExecutionException e) { throw new AssertionError(e.getCause()); } ...
var executorService = Executors.newFixedThreadPool(2); var callables = new ArrayList<Callable<Integer>>(); IntStream.range(1, 100).forEach(i -> callables.add(() -> bigComputation(i))); var futures = executorService.invokeAll(callables); for (var future : futures) { switch (future.state()) { case RUNNING -> throw new AssertionError("should not be there"); case SUCCESS -> System.out.println(future.resultNow()); case FAILED -> System.out.println(future.exceptionNow()); case CANCELLED -> System.out.println("cancelled"); } } ...
var executorService = Executors.newFixedThreadPool(2); var callables = new ArrayList<Callable<Integer>>(); IntStream.range(1,100).forEach(i -> callables.add(() -> bigComputation(i))); try { System.out.println(executorService.invokeAny(callables)); } catch (ExecutionException e) { throw new AssertionError(e); }
ExecutorService
shutdown()
interdit la soumission de nouvelles tâches et arrête les threads quand toutes les tâches ont été
traitées.shutdownNow()
essaie d'interrompre tous les worker-threads. Si les tâches ne répondent pas à l'interrupt()
, alors les worker-threads ne seront jamais arrêtés.