Guide de java.util.concurrent.Future

1. Vue d'ensemble

Dans cet article, nous allons en apprendre davantage sur Future . Une interface qui existe depuis Java 1.5 et qui peut être très utile lorsque vous travaillez avec des appels asynchrones et un traitement simultané.

2. Créer des futurs

En termes simples, la classe Future représente un résultat futur d'un calcul asynchrone - un résultat qui apparaîtra éventuellement dans le Future une fois le traitement terminé.

Voyons comment écrire des méthodes qui créent et retournent une instance Future .

Les méthodes de longue durée sont de bons candidats pour le traitement asynchrone et l' interface Future . Cela nous permet d'exécuter un autre processus pendant que nous attendons la fin de la tâche encapsulée dans Future .

Voici quelques exemples d'opérations qui tireraient parti de la nature asynchrone de Future :

  • processus intensifs de calcul (calculs mathématiques et scientifiques)
  • manipulation de grandes structures de données (big data)
  • appels de méthodes à distance (téléchargement de fichiers, mise au rebut HTML, services Web).

2.1. Mettre en œuvre des contrats à terme avec FutureTask

Pour notre exemple, nous allons créer une classe très simple qui calcule le carré d'un entier . Cela ne correspond certainement pas à la catégorie des méthodes «de longue durée», mais nous allons lui mettre un appel Thread.sleep () pour qu'il dure 1 seconde pour se terminer:

public class SquareCalculator { private ExecutorService executor = Executors.newSingleThreadExecutor(); public Future calculate(Integer input) { return executor.submit(() -> { Thread.sleep(1000); return input * input; }); } }

Le bit de code qui effectue réellement le calcul est contenu dans la méthode call () , fournie sous forme d'expression lambda. Comme vous pouvez le voir, il n'y a rien de spécial à ce sujet, à l'exception de l' appel sleep () mentionné précédemment.

Cela devient plus intéressant lorsque nous portons notre attention sur l'utilisation de Callable et ExecutorService .

Callable est une interface représentant une tâche qui renvoie un résultat et possède une seule méthode call () . Ici, nous en avons créé une instance à l'aide d'une expression lambda.

Créer une instance de Callable ne nous emmène nulle part, nous devons quand même passer cette instance à un exécuteur qui se chargera de démarrer cette tâche dans un nouveau thread et nous rendra le précieux objet Future . C'est là qu'intervient ExecutorService .

Il y a plusieurs façons d'obtenir une instance d' ExecutorService , la plupart d'entre elles sont fournies par les méthodes de fabrique statique de la classe utilitaire Executors . Dans cet exemple, nous avons utilisé le newSingleThreadExecutor () de base , qui nous donne un ExecutorService capable de gérer un seul thread à la fois.

Une fois que nous avons un objet ExecutorService , nous devons simplement appeler submit () en passant notre Callable comme argument. submit () se chargera du démarrage de la tâche et retournera un objet FutureTask , qui est une implémentation de l' interface Future .

3. Consommer des contrats à terme

Jusqu'à présent, nous avons appris à créer une instance de Future .

Dans cette section, nous allons apprendre à travailler avec cette instance en explorant toutes les méthodes qui font partie de l'API de Future .

3.1. Utiliser isDone () et get () pour obtenir des résultats

Nous devons maintenant appeler Calculate () et utiliser le Future retourné pour obtenir le résultat Integer . Deux méthodes de l' API Future nous aideront dans cette tâche.

Future.isDone () nous indique si l'exécuteur a fini de traiter la tâche. Si la tâche est terminée, elle retournera true sinon, elle retournera false .

La méthode qui renvoie le résultat réel du calcul est Future.get () . Notez que cette méthode bloque l'exécution jusqu'à ce que la tâche soit terminée, mais dans notre exemple, ce ne sera pas un problème puisque nous vérifierons d'abord si la tâche est terminée en appelant isDone () .

En utilisant ces deux méthodes, nous pouvons exécuter un autre code en attendant la fin de la tâche principale:

Future future = new SquareCalculator().calculate(10); while(!future.isDone()) { System.out.println("Calculating..."); Thread.sleep(300); } Integer result = future.get();

Dans cet exemple, nous écrivons un message simple sur la sortie pour informer l'utilisateur que le programme effectue le calcul.

La méthode get () bloquera l'exécution jusqu'à ce que la tâche soit terminée. Mais nous n'avons pas à nous en soucier puisque notre exemple n'arrive qu'au point où get () est appelé après s'être assuré que la tâche est terminée. Ainsi, dans ce scénario, future.get () reviendra toujours immédiatement.

Il est à noter que get () a une version surchargée qui prend un timeout et un TimeUnit comme arguments:

Integer result = future.get(500, TimeUnit.MILLISECONDS);

La différence entre get (long, TimeUnit) et get () est que le premier lèvera une TimeoutException si la tâche ne revient pas avant le délai d'expiration spécifié.

3.2. Annulation d'un avenir avec annulation ()

Suppose we've triggered a task but, for some reason, we don't care about the result anymore. We can use Future.cancel(boolean) to tell the executor to stop the operation and interrupt its underlying thread:

Future future = new SquareCalculator().calculate(4); boolean canceled = future.cancel(true);

Our instance of Future from the code above would never complete its operation. In fact, if we try to call get() from that instance, after the call to cancel(), the outcome would be a CancellationException. Future.isCancelled() will tell us if a Future was already canceled. This can be quite useful to avoid getting a CancellationException.

It is possible that a call to cancel() fails. In that case, its returned value will be false. Notice that cancel() takes a boolean value as an argument – this controls whether the thread executing this task should be interrupted or not.

4. More Multithreading With Thread Pools

Our current ExecutorService is single threaded since it was obtained with the Executors.newSingleThreadExecutor. To highlight this “single threadness”, let's trigger two calculations simultaneously:

SquareCalculator squareCalculator = new SquareCalculator(); Future future1 = squareCalculator.calculate(10); Future future2 = squareCalculator.calculate(100); while (!(future1.isDone() && future2.isDone())) { System.out.println( String.format( "future1 is %s and future2 is %s", future1.isDone() ? "done" : "not done", future2.isDone() ? "done" : "not done" ) ); Thread.sleep(300); } Integer result1 = future1.get(); Integer result2 = future2.get(); System.out.println(result1 + " and " + result2); squareCalculator.shutdown();

Now let's analyze the output for this code:

calculating square for: 10 future1 is not done and future2 is not done future1 is not done and future2 is not done future1 is not done and future2 is not done future1 is not done and future2 is not done calculating square for: 100 future1 is done and future2 is not done future1 is done and future2 is not done future1 is done and future2 is not done 100 and 10000

It is clear that the process is not parallel. Notice how the second task only starts once the first task is completed, making the whole process take around 2 seconds to finish.

To make our program really multi-threaded we should use a different flavor of ExecutorService. Let's see how the behavior of our example changes if we use a thread pool, provided by the factory method Executors.newFixedThreadPool():

public class SquareCalculator { private ExecutorService executor = Executors.newFixedThreadPool(2); //... }

With a simple change in our SquareCalculator class now we have an executor which is able to use 2 simultaneous threads.

If we run the exact same client code again, we'll get the following output:

calculating square for: 10 calculating square for: 100 future1 is not done and future2 is not done future1 is not done and future2 is not done future1 is not done and future2 is not done future1 is not done and future2 is not done 100 and 10000

This is looking much better now. Notice how the 2 tasks start and finish running simultaneously, and the whole process takes around 1 second to complete.

There are other factory methods that can be used to create thread pools, like Executors.newCachedThreadPool() that reuses previously used Threads when they are available, and Executors.newScheduledThreadPool() which schedules commands to run after a given delay.

For more information about ExecutorService, read our article dedicated to the subject.

5. Overview of ForkJoinTask

ForkJoinTask is an abstract class which implements Future and is capable of running a large number of tasks hosted by a small number of actual threads in ForkJoinPool.

In this section, we are going to quickly cover the main characteristics of ForkJoinPool. For a comprehensive guide about the topic, check our Guide to the Fork/Join Framework in Java.

Then the main characteristic of a ForkJoinTask is that it usually will spawn new subtasks as part of the work required to complete its main task. It generates new tasks by calling fork() and it gathers all results with join(), thus the name of the class.

There are two abstract classes that implement ForkJoinTask: RecursiveTask which returns a value upon completion, and RecursiveAction which doesn't return anything. As the names imply, those classes are to be used for recursive tasks, like for example file-system navigation or complex mathematical computation.

Let's expand our previous example to create a class that, given an Integer, will calculate the sum squares for all its factorial elements. So, for instance, if we pass the number 4 to our calculator, we should get the result from the sum of 4² + 3² + 2² + 1² which is 30.

First of all, we need to create a concrete implementation of RecursiveTask and implement its compute() method. This is where we'll write our business logic:

public class FactorialSquareCalculator extends RecursiveTask { private Integer n; public FactorialSquareCalculator(Integer n) { this.n = n; } @Override protected Integer compute() { if (n <= 1) { return n; } FactorialSquareCalculator calculator = new FactorialSquareCalculator(n - 1); calculator.fork(); return n * n + calculator.join(); } }

Notice how we achieve recursiveness by creating a new instance of FactorialSquareCalculator within compute(). By calling fork(), a non-blocking method, we ask ForkJoinPool to initiate the execution of this subtask.

The join() method will return the result from that calculation, to which we add the square of the number we are currently visiting.

Now we just need to create a ForkJoinPool to handle the execution and thread management:

ForkJoinPool forkJoinPool = new ForkJoinPool(); FactorialSquareCalculator calculator = new FactorialSquareCalculator(10); forkJoinPool.execute(calculator);

6. Conclusion

In this article, we had a comprehensive view of the Future interface, visiting all its methods. We've also learned how to leverage the power of thread pools to trigger multiple parallel operations. The main methods from the ForkJoinTask class, fork() and join() were briefly covered as well.

We have many other great articles on parallel and asynchronous operations in Java. Here are three of them that are closely related to the Future interface (some of them are already mentioned in the article):

  • Guide to CompletableFuture – an implementation of Future with many extra features introduced in Java 8
  • Guide du framework Fork / Join en Java - en savoir plus sur ForkJoinTask que nous avons couvert dans la section 5
  • Guide de Java ExecutorService - dédié à l' interface ExecutorService

Vérifiez le code source utilisé dans cet article dans notre référentiel GitHub.