Concurrency in Java: CompletableFuture and its use

  • 4.4/5
  • 5856
  • Jul 20, 2024

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
Index
Modern Java - What’s new in Java 9 to Java 17

32 min

What is differences between JDK, JVM and JRE ?

2 min

What is ClassLoader in Java ?

2 min

Object Oriented Programming (OOPs) Concept

17 min

Concurrency in Java: Creating and Starting a Thread

12 min

Concurrency in Java: Interrupting and Joining Threads

5 min

Concurrency in Java: Race condition, critical section, and atomic operations

13 min

Concurrency in Java: Reentrant, Read/Write and Stamped Locks

11 min

Concurrency in Java: "synchronized" and "volatile" keywords

10 min

Concurrency in Java: using wait(), notify() and notifyAll()

6 min

Concurrency in Java: What is "Semaphore" and its use?

2 min

Concurrency in Java: CompletableFuture and its use

18 min

Concurrency in Java: Producer-consumer problem using BlockingQueue

2 min

Concurrency in Java: Producer-Consumer Problem

2 min

Concurrency in Java: Thread pools, ExecutorService & Future

14 min

Java 8 Lambdas, Functional Interface & "static" and "default" methods

28 min

Method Reference in Java (Instance, Static, and Constructor Reference)

9 min

What’s new in Java 21: A Tour of its Most Exciting Features

14 min

Java Memory Leaks & Heap Dumps (Capturing & Analysis)

9 min

Memory footprint of the JVM (Heap & Non-Heap Memory)

15 min