Skip to main content

Concurrency in Java: CompletableFuture and its use

The "java.util.concurrent.CompletableFuture" class was introduced as part of the Java 8 Concurrency API improvement.

It is an extension to "java.util.concurrent.Future" and may be explicitly completed by setting its value and status.

Along with the "Future" interface, the CompletableFuture also implements the "CompletionStage" interface.

The "CompletionStage" interface defines the contract for asynchronous computation steps (functions and actions) that we can combine with other steps.

The "CompletableFuture" contains more than "fifty" different methods for composing, combining, and executing asynchronous computation steps and handling errors.

1) Run a background task asynchronously

In order to run a background task asynchronously, we can use CompletableFuture.runAsync() method. It accepts a Runnable object as input and returns a CompletableFuture as output.

main running..
ForkJoinPool.commonPool-worker-1 ... long running job completed !!!
main running..

2) Run asynchronously and return the result

The CompletableFuture.runAsync() is used to run tasks that don't return anything.

But to run a task asynchronously that returns a result, we should use CompletableFuture.supplyAsync(). It takes a Supplier as input and returns a CompletableFuture as output.

 .. result returned !!!

The CompletableFuture.get() method blocks until the result is available to the main thread.

3) Using a user-defined thread-pool

By default, CompletableFuture executes these tasks in a thread obtained from the global ForkJoinPool.commonPool().

But it is also possible to create a thread pool and pass it to overloaded versions of the supplyAsync() and runAsync() methods.

main running..
pool-1-thread-1 ... long running job completed !!!
main running..

4) Attaching a callback to the CompletableFuture

The CompletableFuture.get() method blocks until the result is available to the main thread.

But, to build an efficient asynchronous system we should be able to attach a callback to the CompletableFuture which should automatically run when the Future completes.

4.1) Using "thenApply()"

The thenApply() method can be used to process the result of a CompletableFuture when it is available.

It takes a Function as an argument, which means that it takes an argument of type T and returns a result of type R.

main ...running
ForkJoinPool.commonPool-worker-1 ...running
ForkJoinPool.commonPool-worker-1 ...running
Thanks for  .. returning the result !!!

4.2) Using "thenAccept()"

If we don't need to return a value further down the Future chain, we can use a Consumer functional interface instance.

The thenAccept() method accepts a Consumer and returns an instance of the CompletableFuture.

ForkJoinPool.commonPool-worker-1 ...running
ForkJoinPool.commonPool-worker-1 ...running
Thanks for  .. returning the result !!!

4.3) Using "thenRun()"

If we neither need the value of the computation nor want to return some value at the end of the chain, then we can use the thenRun() method.

The thenRun() method accepts a Runnable and returns an instance of the CompletableFuture.

ForkJoinPool.commonPool-worker-1 ...running
 .. I am done !!!
ForkJoinPool.commonPool-worker-1 ...running
 ..thanks, I am done too !!!

All the three methods discussed above have their "async" overloaded versions. These async callback variations can be used to further parallelize the computations by executing the callback tasks in a separate thread.

Furthermore, these methods have overloaded versions that accept an executor, allowing the callback to be executed in a thread from the executor's thread pool.

ForkJoinPool.commonPool-worker-1 ...running
 .. I am done !!!
pool-1-thread-1 ...running
 ..thanks, I am done too !!!

5) Combining two CompletableFutures

5.1) Using "thenCompose()"

The thenCompose() method is used to combine the results of two dependent tasks when the second task is dependent on the completion of the first.

pool-1-thread-1..running
pool-1-thread-1..running
User-101's Account

5.2) Using "thenCombine()"

The thenCombine() method is used to combine the results of two independent tasks when the two tasks are independent of each other's completion.

pool-1-thread-1..running
pool-1-thread-2..running
User's Account and User's Profile

6) Combining multiple CompletableFutures

The thenCompose() and thenCombine() methods can be used to combine two CompletableFutures together.

In order to combine any number of CompletableFutures, we can use either of the following two methods.


6.1) Using "allOf()"

The allOf() static method is used to combine the results of a number of independent tasks, when "none" of the tasks are dependent on any other task.

The return type of this method is CompletableFuture, hence it does not return the combined results of all CompletableFutures.

But we can get the results of all the wrapped CompletableFutures with the help of CompletableFuture.join() method and the Java 8 Streams API.

pool-1-thread-3..running
pool-1-thread-1..running
pool-1-thread-2..running
User's Account User's Profile User's Transactions

The CompletableFuture.join() method is similar to the get() method; the only difference is that it throws an unchecked exception if the Future does not complete normally.

6.2) Using "anyOf()"

The anyOf() static method returns a new CompletableFuture<Object> as soon as any of the given CompletableFutures completes.

pool-1-thread-1..running
pool-1-thread-2..running
pool-1-thread-3..running
I got it in c...a good developer

7) Exception Handling

7.1) Using "handle()" method

The CompletableFuture class allows us to handle exceptions in a special handle() method.

The handle() method has two parameters - the result of a computation (if successful) and the exception thrown (if the computation could not be completed normally).

In the "handle()" method, we can transform the result or recover the exception.

Invalid Username: java.lang.RuntimeException ...

7.2) Using whenComplete() callback

In the whenComplete() method, we have access to the result and exception of the current completable future as arguments, which you can consume and perform your desired action.

However, we cannot transform the current result or exception into another result.

We cannot return a value from the whenComplete() method.

Exception occurred: java.util.concurrent.CompletionException: java.lang.RuntimeException
Exception: java.util.concurrent.ExecutionException: java.lang.RuntimeException

7.3) Using exceptionally() callback

In the exceptionally() method, you only have access to the exception and not the result.

If the CompletableFuture was completed successfully, then the logic inside "exceptionally()" will be skipped.

Exception occurred: java.util.concurrent.CompletionException: java.lang.RuntimeException

Comments

Popular posts from this blog

Deploying Spring Boot microservices on Kubernetes Cluster

This article guides you through the deployment of two Spring Boot microservices, namely "order-service" and "inventory-service," on Kubernetes using "MiniKube" . We will establish communication between them, with "order-service" making calls to an endpoint in "inventory-service." Additionally, we will configure "order-service" to be accessible from the local machine's browser . 1) Create Spring Boot microservices The Spring Boot microservices, "order-service" and "inventory-service," have been developed and can be found in this GitHub repository. If you are interested in learning more about creating Spring Boot REST microservices, please refer to this or this (Reactive) link. 2) Build Docker Images The Docker images for both "order-service" and "inventory-service" have already been generated and deployed on DockerHub, as shown below. codeburps/order-service cod...

Circuit Breaker Pattern with Resilience4J in a Spring Boot Application

Read Also: Spring Cloud Circuit Breaker + Resilience4j Resilience4j is a lightweight fault tolerance library that draws inspiration from Netflix Hystrix but is specifically crafted for functional programming. The library offers higher-order functions, known as decorators , designed to augment any functional interface, lambda expression, or method reference with features such as Circuit Breaker, Rate Limiter, Retry, or Bulkhead . These functionalities can be seamlessly integrated within a project, class, or even applied to a single method. It's possible to layer multiple decorators on any functional interface, lambda expression, or method reference, allowing for versatile and customizable fault tolerance. While numerous annotation-based implementations exist online, this article focuses solely on the reactive approach using router predicates and router functions . How Circuit Breaker Pattern works? In general, a circuit breaker functions as an automatic electrical s...

How to create a basic Spring 6 project using Maven

Below is a step-by-step guide to creating a basic Spring project using Maven. 1) Create a Maven Project Use the following Maven command to create a new Maven project. mvn archetype:generate -DgroupId=com.tb -DartifactId=spring-demo -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false 2) Import in IntelliJ IDEA If you haven't already, open IntelliJ IDEA on your system. Go to "File" > "New" > "Project from Existing Sources..." . In the file dialog, navigate to the directory where your Maven project is located. Select the pom.xml file within the project directory and click "Open." 3) Update pom.xml In total, the application requires the below-mentioned dependencies: 4) Create Spring Configuration Create a Java configuration class that uses annotations to define your Spring beans and their dependencies. This class should be annotated with @Configuration . 5) Create the Main Application C...