Programmation asynchrone en Java

1. Vue d'ensemble

Avec la demande croissante d'écriture de code non bloquant, nous avons besoin de moyens d'exécuter le code de manière asynchrone.

Dans ce didacticiel, nous examinerons quelques façons de réaliser une programmation asynchrone en Java. Nous explorerons également quelques bibliothèques Java qui fournissent des solutions prêtes à l'emploi.

2. Programmation asynchrone en Java

2.1. Fil

Nous pouvons créer un nouveau thread pour effectuer toute opération de manière asynchrone. Avec la sortie des expressions lambda dans Java 8, c'est plus propre et plus lisible.

Créons un nouveau thread qui calcule et imprime la factorielle d'un nombre:

int number = 20; Thread newThread = new Thread(() -> { System.out.println("Factorial of " + number + " is: " + factorial(number)); }); newThread.start();

2.2. FutureTask

Depuis Java 5, l' interface Future fournit un moyen d'effectuer des opérations asynchrones à l'aide de FutureTask .

Nous pouvons utiliser la méthode submit de l' ExecutorService pour effectuer la tâche de manière asynchrone et renvoyer l'instance de FutureTask .

Alors, trouvons la factorielle d'un nombre:

ExecutorService threadpool = Executors.newCachedThreadPool(); Future futureTask = threadpool.submit(() -> factorial(number)); while (!futureTask.isDone()) { System.out.println("FutureTask is not finished yet..."); } long result = futureTask.get(); threadpool.shutdown();

Ici, nous avons utilisé la méthode isDone fournie par l' interface Future pour vérifier si la tâche est terminée. Une fois terminé, nous pouvons récupérer le résultat en utilisant la méthode get .

2.3. CompletableFutur

Java 8 a introduit CompletableFuture avec une combinaison de Future et CompletionStage . Il fournit diverses méthodes telles que supplyAsync , runAsync et thenApplyAsync pour la programmation asynchrone.

Alors, utilisons CompletableFuture à la place de FutureTask pour trouver la factorielle d'un nombre:

CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> factorial(number)); while (!completableFuture.isDone()) { System.out.println("CompletableFuture is not finished yet..."); } long result = completableFuture.get();

Nous n'avons pas besoin d'utiliser explicitement ExecutorService . Le CompletableFuture utilise en interne ForkJoinPool pour gérer la tâche de manière asynchrone . Par conséquent, cela rend notre code beaucoup plus propre.

3. Goyave

Guava fournit la classe ListenableFuture pour effectuer des opérations asynchrones.

Tout d'abord, nous allons ajouter la dernière dépendance goyave Maven:

 com.google.guava guava 28.2-jre 

Ensuite, trouvons la factorielle d'un nombre en utilisant ListenableFuture :

ExecutorService threadpool = Executors.newCachedThreadPool(); ListeningExecutorService service = MoreExecutors.listeningDecorator(threadpool); ListenableFuture guavaFuture = (ListenableFuture) service.submit(()-> factorial(number)); long result = guavaFuture.get();

Ici, la classe MoreExecutors fournit l'instance de la classe ListeningExecutorService . Ensuite, la méthode ListeningExecutorService.submit exécute la tâche de manière asynchrone et renvoie l'instance de ListenableFuture .

Guava a également une classe Futures qui fournit des méthodes comme submitAsync , scheduleAsync et transformAsync pour enchaîner les ListenableFutures similaires à CompletableFuture.

Par exemple, voyons comment utiliser Futures.submitAsync à la place de la méthode ListeningExecutorService.submit :

ListeningExecutorService service = MoreExecutors.listeningDecorator(threadpool); AsyncCallable asyncCallable = Callables.asAsyncCallable(new Callable() { public Long call() { return factorial(number); } }, service); ListenableFuture guavaFuture = Futures.submitAsync(asyncCallable, service);

Ici, la méthode submitAsync nécessite un argument de AsyncCallable , qui est créé à l'aide de la classe Callables .

De plus, la classe Futures fournit la méthode addCallback pour enregistrer les rappels de réussite et d'échec:

Futures.addCallback( factorialFuture, new FutureCallback() { public void onSuccess(Long factorial) { System.out.println(factorial); } public void onFailure(Throwable thrown) { thrown.getCause(); } }, service);

4. EA Async

Electronic Arts a apporté la fonctionnalité async-await de .NET à l'écosystème Java via la bibliothèque ea-async .

La bibliothèque permet d'écrire du code asynchrone (non bloquant) de manière séquentielle. Par conséquent, il facilite la programmation asynchrone et évolue naturellement.

Tout d'abord, nous allons ajouter la dernière dépendance ea-async Maven au pom.xml :

 com.ea.async ea-async 1.2.3 

Ensuite, transformons le code CompletableFuture précédemment discuté en utilisant la méthode await fournie par la classe Async d'EA :

static { Async.init(); } public long factorialUsingEAAsync(int number) { CompletableFuture completableFuture = CompletableFuture.supplyAsync(() -> factorial(number)); long result = Async.await(completableFuture); }

Ici, nous appelons la méthode Async.init dans le bloc statique pour initialiser l' instrumentation d'exécution Async .

L' instrumentation Async transforme le code à l'exécution et réécrit l'appel à la méthode await , pour se comporter de la même manière que l'utilisation de la chaîne CompletableFuture .

Par conséquent, l'appel à la await méthode est similaire à appeler Future.join.

Nous pouvons utiliser le paramètre JVM - javaagent pour l'instrumentation à la compilation. Ceci est une alternative à la méthode Async.init :

java -javaagent:ea-async-1.2.3.jar -cp  

Examinons un autre exemple d'écriture séquentielle de code asynchrone.

Tout d'abord, nous allons effectuer quelques opérations de chaîne de manière asynchrone en utilisant les méthodes de composition comme thenComposeAsync et thenAcceptAsync de la classe CompletableFuture :

CompletableFuture completableFuture = hello() .thenComposeAsync(hello -> mergeWorld(hello)) .thenAcceptAsync(helloWorld -> print(helloWorld)) .exceptionally(throwable -> { System.out.println(throwable.getCause()); return null; }); completableFuture.get();

Then, we can transform the code using EA's Async.await():

try { String hello = await(hello()); String helloWorld = await(mergeWorld(hello)); await(CompletableFuture.runAsync(() -> print(helloWorld))); } catch (Exception e) { e.printStackTrace(); }

The implementation resembles the sequential blocking code. However, the await method doesn't block the code.

As discussed, all calls to the await method will be rewritten by the Async instrumentation to work similarly to the Future.join method.

So, once the asynchronous execution of the hello method is finished, the Future result is passed to the mergeWorld method. Then, the result is passed to the last execution using the CompletableFuture.runAsync method.

5. Cactoos

Cactoos is a Java library based on object-oriented principles.

It is an alternative to Google Guava and Apache Commons that provides common objects for performing various operations.

First, let's add the latest cactoos Maven dependency:

 org.cactoos cactoos 0.43 

The library provides an Async class for asynchronous operations.

So, we can find the factorial of a number using the instance of Cactoos's Async class:

Async asyncFunction = new Async(input -> factorial(input)); Future asyncFuture = asyncFunction.apply(number); long result = asyncFuture.get();

Here, the apply method executes the operation using the ExecutorService.submit method and returns an instance of the Future interface.

Similarly, the Async class has the exec method that provides the same feature without a return value.

Note: the Cactoos library is in the initial stages of development and may not be appropriate for production use yet.

6. Jcabi-Aspects

Jcabi-Aspects provides the @Async annotation for asynchronous programming through AspectJ AOP aspects.

First, let's add the latest jcabi-aspects Maven dependency:

 com.jcabi jcabi-aspects 0.22.6  

The jcabi-aspects library requires AspectJ runtime support. So, we'll add the aspectjrt Maven dependency:

 org.aspectj aspectjrt 1.9.5  

Next, we'll add the jcabi-maven-plugin plugin that weaves the binaries with AspectJ aspects. The plugin provides the ajc goal that does all the work for us:

 com.jcabi jcabi-maven-plugin 0.14.1    ajc      org.aspectj aspectjtools 1.9.1   org.aspectj aspectjweaver 1.9.1   

So, we're all set to use the AOP aspects for asynchronous programming:

@Async @Loggable public Future factorialUsingAspect(int number) { Future factorialFuture = CompletableFuture.completedFuture(factorial(number)); return factorialFuture; }

When we compile the code, the library will inject AOP advice in place of the @Async annotation through AspectJ weaving, for the asynchronous execution of the factorialUsingAspect method.

So, let's compile the class using the Maven command:

mvn install

The output from the jcabi-maven-plugin may look like:

 --- jcabi-maven-plugin:0.14.1:ajc (default) @ java-async --- [INFO] jcabi-aspects 0.18/55a5c13 started new daemon thread jcabi-loggable for watching of @Loggable annotated methods [INFO] Unwoven classes will be copied to /tutorials/java-async/target/unwoven [INFO] jcabi-aspects 0.18/55a5c13 started new daemon thread jcabi-cacheable for automated cleaning of expired @Cacheable values [INFO] ajc result: 10 file(s) processed, 0 pointcut(s) woven, 0 error(s), 0 warning(s)

We can verify if our class is woven correctly by checking the logs in the jcabi-ajc.log file, generated by the Maven plugin:

Join point 'method-execution(java.util.concurrent.Future com.baeldung.async.JavaAsync.factorialUsingJcabiAspect(int))' in Type 'com.baeldung.async.JavaAsync' (JavaAsync.java:158) advised by around advice from 'com.jcabi.aspects.aj.MethodAsyncRunner' (jcabi-aspects-0.22.6.jar!MethodAsyncRunner.class(from MethodAsyncRunner.java))

Then, we'll run the class as a simple Java application, and the output will look like:

17:46:58.245 [main] INFO com.jcabi.aspects.aj.NamedThreads - jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-loggable for watching of @Loggable annotated methods 17:46:58.355 [main] INFO com.jcabi.aspects.aj.NamedThreads - jcabi-aspects 0.22.6/3f0a1f7 started new daemon thread jcabi-async for Asynchronous method execution 17:46:58.358 [jcabi-async] INFO com.baeldung.async.JavaAsync - #factorialUsingJcabiAspect(20): '[email protected][Completed normally]' in 44.64µs

So, we can see a new daemon thread jcabi-async is created by the library that performed the task asynchronously.

Similarly, the logging is enabled by the @Loggable annotation provided by the library.

7. Conclusion

In this article, we've seen a few ways of asynchronous programming in Java.

To begin with, we explored Java's in-built features like FutureTask and CompletableFuture for asynchronous programming. Then, we've seen a few libraries like EA Async and Cactoos with out-of-the-box solutions.

Also, we examined the support of performing tasks asynchronously using Guava's ListenableFuture and Futures classes. Last, we explored the jcabi-AspectJ library that provides AOP features through its @Async annotation for asynchronous method calls.

Comme d'habitude, toutes les implémentations de code sont disponibles sur sur GitHub.