Recently, I was given a task in which I had to call the OMDB API to fetch the box office value of 10 top-rated movies on the fly. Making 10 REST API calls synchronously and combining the results would definitely be a time-consuming task. Hence I thought of making parallel API calls using CompletableFuture
provided by the java.util.concurrent
package.
Table of Contents
- Introduction to Future
- Synchronous vs Asynchronous Rest API Calls
- Get vs Join
- RunAsync vs SupplyAsync
- Conclusion
Introduction to Future
A Future
represents the result of an asynchronous computation. Methods are provided to check if the computation is complete, wait for its completion, and retrieve the result. The result can only be retrieved using the method get
when the computation has been completed, blocking if necessary until it is ready. Cancellation is performed by the cancel
method.
Limitations
- It cannot be manually completed by setting its value and status
- No further action can be performed on a Future’s result without blocking,
- Multiple futures cannot be combined together and then run some function after all of them are complete.
- Multiple futures cannot be chained in order to send the result of one Future to another after the completion.
- It does not have any exception-handling construct.
CompletableFuture
CompletableFuture was introduced as an extension of Future API in order to overcome its limitations. It is a Future
that may be explicitly completed (setting its value and status) and may be used as a CompletionStage
, supporting dependent functions and actions that trigger upon its completion.
Synchronous vs Asynchronous Rest API Calls
Let’s see the example in which we will make multiple REST API calls in synchronous and asynchronous methods and see the performance difference.
@Override
public List<MovieDTO> findTop10RatedMovies() {
List<Movie> movies = movieRepository.findTop10RatedMovies(PageRequest.of(0, 10));
// Asynchronous execution
Executor executor = Executors.newFixedThreadPool(10);
long start = System.currentTimeMillis();
var futureMovies = movies.stream().map(m -> CompletableFuture.supplyAsync(() ->
omdbService.enrichMovieWithBoxOfficeValue(m), executor)).collect(toList());
var topMovies = futureMovies.stream().map(CompletableFuture::join).collect(toList());
long end = System.currentTimeMillis();
System.out.printf("The future operation took %s ms%n", end - start);
// Synchronous execution
start = System.currentTimeMillis();
topMovies = movies.stream().map(m -> omdbService.enrichMovieWithBoxOfficeValue(m)).collect(Collectors.toList());
end = System.currentTimeMillis();
System.out.printf("The normal operation took %s ms%n", end - start);
topMovies.sort((o1, o2) -> o2.getBoxOffice().compareTo(o1.getBoxOffice()));
return topMovies;
}
Output
The future operation took 580 ms
The normal operation took 2221 ms
As you can see, the synchronous execution took 4X more time compared to asynchronous execution. I have used CompletableFuture
instead of Future
for the asynchronous execution Since I wanted to combine multiple futures together and perform the sorting after all of them are complete which is not possible with the latter.
Now, let’s see in detail to understand how the REST API calls are being made parallelly. Firstly, we need to create a fixed thread pool for asynchronous execution.
Executor executor = Executors.newFixedThreadPool(10);
Secondly, we must create a list of CompletableFuture
objects for each movie. Hence, we are creating a stream of movies and then creating a CompletableFuture
object for each movie with the help of CompletableFuture.supplyAsync()
method.
CompletableFuture.supplyAsync(Supplier supplier, Executor executor)
method returns a new CompletableFuture
that is asynchronously completed by a task running in the given executor
with the value obtained by calling the given Supplier
.
It expects the following parameters:
supplier
– a function returning the value to be used to complete the returnedCompletableFuture
executor
– the executor to use for asynchronous execution
var futureMovies = movies.stream().map(m -> CompletableFuture.supplyAsync(() ->
omdbService.enrichMovieWithBoxOfficeValue(m), executor)).collect(toList());
Note 1: If you don’t want to create a thread pool, then you can use the other variant of
CompletableFuture.supplyAsync
() method which expects only theSupplier
. It returns a newCompletableFuture
that is asynchronously completed by a task running in theForkJoinPool.commonPool()
with the value obtained by calling the givenSupplier
.
Note 2: If you want to execute asynchronous tasks that don’t return anything, then you can use
CompletableFuture.runAsync
() method. We will explore the differences between these 2 in the next section.
Finally, we need to call the join method of CompletableFuture
which returns the result value when complete. Then the results can be collected into a list.
var topMovies = futureMovies.stream().map(CompletableFuture::join).collect(toList());
Note: You can also use the
CompletableFuture.get()
that waits if necessary for the computation to complete and then retrieves its result.
Get vs Join
- The
get
method is from theFuture
interface while thejoin
method is fromCompletableFuture
. - The
get
throws checked exceptionsInterruptedException
andExecutionException
which needs to be handled explicitly whilejoin
throws an Unchecked exception. - There is also one more variant of
get
which takes wait time as an argument and waits for at most the provided wait time. However, this is not supported by thejoin
method.
RunAsync vs SupplyAsync
runAsync
takes Runnable as an input parameter and returnsCompletableFuture<Void>
, which means it does not return any results whilesupplyAsync
takes the Supplier as an argument and returns theCompletableFuture<U>
with result value.- If you want the result to be returned, then use
supplyAsync
or if you just want to run an async action, then userunAsync
method.
Conclusion
That’s all folks. In this article, we have seen how to make Multiple Rest API Calls in Parallel using CompletableFuture
.
Thank you for reading.