In Java, it has always been easy to execute a task in separate thread using Runnable/Callable. It helps in offloading task off the main thread (eg: In Android, network requests are not allowed be executed in a UI thread).
With ExecutorService, it is even easier to manage multiple independent tasks in separate a thread/thread-pool.
Though, if we want to execute multiple related/dependant tasks one after another,
the flow is not completely asynchronous. It is because the future.get method (which is used to wait for a task to be completed) is a blocking operation.
This is exactly the use-case CompletableFuture was built for; to chain multiple dependant tasks.
Another way to think of it is Java Streams where each step can be executed in async manner (in a separate thread).
You can explore more about CompletableFuture in this talk.
Now that we understand its use case, let’s walk-through its source code.
The USP of this class is chained calls (aka fluid API). For this to work, each call should return
instance of a CompletableFuture so that same methods can be again applied on the return type.
The compute methods have 2 versions,
one which takes ExecutorService instance as an argument to run the task.
one which only supplies the task, and CompletableFuture uses its own ExecutorService to run it.
There are 4 types of primary/compute calls supported.
Supply: Used to supply value. No input, provides output.
Apply: Used to apply a function on an input. Takes input, provides output.
Accept: Used to accept a value and run a function using the same. Takes input, doesn’t provide output.
Run: Used to run a function. No input, no output.
Each of these are represented as classes: AsyncSupply, UniApply, UniAccept and UniRun.
Lets start with CompletableFuture.supplyAsync. It is responsible for
executing the supplied task
setting the result value or exception or null (in case its a Run method which doesn’t have any output)
calling the postComplete method so that next CompletableFuture can be called
Setting the result
The result of a function (output) if any, is applied to a single field using CAS (compare-and-swap) operation.
This set result is then used by subsequent CompletableFutures as input
Other chained methods
The other chained methods (thenApply, thenAccept, thenRun) are slightly different than the supply method.
They are very similar to each other though. Lets take a look at one of them, say thenApplyAsync method.
Running the chained methods
Notice that the CompletableFuture dependencies are only added to the stack if the previous ones are not completed yet.
In such cases, the stack has to be popped one after other, and run.
Lets revisit the postComplete method from the AsyncSupply’s run method.
CompletableFuture also exposes a public method to manually set the value from the outside.
Similarly it also allows a CF instance to be cancelled manually.
CompletableFuture class source is ~2400 lines long. It also has massive API.
Also, sadly, of all the JDK source classes I’ve read, this one was the most difficult to understand.
Anyways, it was satisfying to finally get it.
We skipped all teh compound methods which uses multiple CompletableFuture instances in combinations of and, or, any etc.
Though, hopefully, once you get the basic flow detailed above, the rest should come relatively easy.
Hit me up in the comments for any queries or corrections.