Programación asincrónica en Java

1. Información general

Con la creciente demanda de escribir código sin bloqueo, necesitamos formas de ejecutar el código de forma asincrónica.

En este tutorial, veremos algunas formas de lograr la programación asincrónica en Java. Además, exploraremos algunas bibliotecas de Java que brindan soluciones listas para usar.

2. Programación asincrónica en Java

2.1. Hilo

Podemos crear un nuevo hilo para realizar cualquier operación de forma asincrónica. Con el lanzamiento de expresiones lambda en Java 8, es más limpio y legible.

Creemos un nuevo hilo que calcule e imprima el factorial de un número:

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

2.2. FutureTask

Desde Java 5, la interfaz Future proporciona una forma de realizar operaciones asincrónicas utilizando FutureTask .

Podemos usar el método de envío de ExecutorService para realizar la tarea de forma asincrónica y devolver la instancia de FutureTask .

Entonces, encontremos el factorial de un número:

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();

Aquí, hemos utilizado el método isDone proporcionado por la interfaz Future para verificar si la tarea se completó. Una vez terminado, podemos recuperar el resultado usando el método get .

2.3. CompletableFuturo

Java 8 introdujo CompletableFuture con una combinación de Future y CompletionStage . Proporciona varios métodos como supplyAsync , runAsync y thenApplyAsync para la programación asincrónica.

Entonces, usemos CompletableFuture en lugar de FutureTask para encontrar el factorial de un número:

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

No necesitamos usar ExecutorService explícitamente. El CompletableFuture utiliza internamente ForkJoinPool para manejar la tarea de forma asíncrona . Por lo tanto, hace que nuestro código sea mucho más limpio.

3. Guayaba

Guava proporciona la clase ListenableFuture para realizar operaciones asincrónicas.

Primero, agregaremos la última dependencia de guava Maven:

 com.google.guava guava 28.2-jre 

Luego, busquemos el factorial de un número usando ListenableFuture :

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

Aquí, la clase MoreExecutors proporciona la instancia de la clase ListeningExecutorService . Luego, el método ListeningExecutorService.submit realiza la tarea de forma asincrónica y devuelve la instancia de ListenableFuture .

Guava también tiene una clase Futures que proporciona métodos como submitAsync , scheduleAsync y transformAsync para encadenar ListenableFutures de forma similar a CompletableFuture.

Por ejemplo, veamos cómo usar Futures.submitAsync en lugar del método 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);

Aquí, el método submitAsync requiere un argumento de AsyncCallable , que se crea usando la clase Callables .

Además, la clase Futures proporciona el método addCallback para registrar las devoluciones de llamada correctas y fallidas :

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 trajo la función async-await de .NET al ecosistema Java a través de la biblioteca ea-async .

La biblioteca permite escribir código asincrónico (sin bloqueo) secuencialmente. Por lo tanto, facilita la programación asincrónica y se escala de forma natural.

Primero, agregaremos la última dependencia de ea-async Maven al pom.xml :

 com.ea.async ea-async 1.2.3 

Luego, transformemos el código CompletableFuture discutido anteriormente utilizando el método de espera proporcionado por la clase Async de EA :

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

Aquí, hacemos una llamada al método Async.init en el bloque estático para inicializar la instrumentación en tiempo de ejecución Async .

La instrumentación asincrónica transforma el código en tiempo de ejecución y reescribe la llamada al método await , para comportarse de manera similar al uso de la cadena de CompletableFuture .

Por lo tanto, la llamada a la Await método es similar a llamar Future.join.

Podemos usar el parámetro - javaagent JVM para la instrumentación en tiempo de compilación. Esta es una alternativa al método Async.init :

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

Examinemos otro ejemplo de escritura de código asincrónico secuencialmente.

Primero, realizaremos algunas operaciones en cadena de forma asincrónica utilizando los métodos de composición como thenComposeAsync y thenAcceptAsync de la clase 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.

Como de costumbre, todas las implementaciones de código están disponibles en GitHub.