Waiting for background tasks to finish using the CompletableFuture class in Java

In this post we saw how to wait for a number background tasks to finish using the CountDownLatch class. The starting point for the discussion was the following situation:

Imagine that you execute a number of long running methods. Also, let’s say that the very last time consuming process depends on the previous processes, let’s call them prerequisites. The dependence is “sequential” meaning that the final stage should only run if the prerequisites have all completed and returned. The first implementation may very well be sequential where the long running methods are called one after the other and each of them blocks the main thread.

However, in case the prerequisites can be executed independently then there’s a much better solution: we can execute them in parallel instead. Independence in this case means that prerequisite A doesn’t need any return value from prerequisite B in which case parallel execution of A and B is not an option.

In this post we’ll look at an alternative solution using the CompletableFuture class. It is way more versatile than CountDownLatch which is really only sort of like a simple lock object. CompletableFuture offers a wide range of possibilities to organise your threads with a fluent API. Here we’ll start off easy with a simple application of this class.

Let’s first repeat what kind of interfaces and implementations we work with in the demo.

A sequential solution would simply call each printer service to print the message one after the other with the UnchangedMessagePrinterService coming last. The total execution time will be around 15 seconds. It is the sum of all Thread.sleep wait times in the 5 message printers.

The threaded solution

We can immediately see that the prerequisite message printers can be called in parallel. There’s nothing in e.g. ReversedMessagePrinterService that depends on AnnoyedMessagePrinterService.

The CompletableFuture class is similar to Futures but it offers a lot more functions and extensions to arrange our threads. A completable future can be void, i.e. return nothing, or it can have a return value. CompletableFuture has a number of static methods that help with the construction of threads. In the below example we’ll see the following functions in action:

runAsync: accepts a Runnable, i.e. a class with a run method that’s called when the thread starts. It also optionally accepts a thread pool

allOf: accepts an array of tasks that must be completed. This is where the prerequisite tasks are waited upon in parallel

thenRunAsync: an example of the fluent API behind CompletableFuture. This function also accepts a Runnable and this is where we’ll put the last continuation task, i.e. the UnchangedMessagePrinterService message printer

exceptionally: a function that deals with exceptions in the preceding completable futures

The below example calls the various message printers using Lambda expressions. If you’re new to lambdas in Java then start here.

Note how the allOf, thenRunAsync and exceptionally functions can be called in succession in a fluent way. The exceptionally function also returns a void completable future. It acts as a “placeholder” task in case of an exception. It’s fine to return null if we only want to print the exception message.