Since the beginning of multicore processors, the most effective way to increase the performance of an application is to fully utilize all processor cores. Running tasks which might take longer to return asynchronously (in a separate thread) is beneficial because the main thread can continue to do useful work while your result is being calculated. Using blocking calls can considerably decrease throughput as the program is wasting useful clock cycles on waiting for calls to return. Achieving greater throughput is also possible by splitting a long running task into smaller tasks and run them in parallel.
CompletableFuture is an implementation of the Future interface but with a modern twist. It takes advantage of functional operations to promote asynchronous/event-driven programming model. You should have some knowledge of threads in Java to be able to follow along.
To better understand CompletableFutures, let’s have a look at Futures first. They model asynchronous computations and provide a reference to its result which might not be available immediately. Methods are provided to get the result or check if the computation has finished. Calls to methods returning Futures can return immediately and the calling thread can continue to do useful work. All in all, Futures are friendlier to use than lower-level threads.
To create a future you need to wrap an operation in a Callable and pass it to an ExecutorService.
In the previous example a Callable is passed to an ExecutorService. Since Java 8, instead of an anonymous class you can use a lambda expression to define the body of the Callable. The
delay method is used to simulate a network call which delays the thread for 5 seconds. ExecutorService will return a Future which can be used later to retrieve the result. During the time the network call is processed, it is possible to do other useful work. Finally calling
get() on the Future will get its result. If it’s not available yet, the execution is blocked until the result becomes available. But what if the thread never finishes or takes too long to compute? The get method can accept a timeout as an argument. You can also use the
isDone() method to check if the task has completed.
But what if you’d like to take the result of an asynchronous task and immediately do other computations with it and then, say, combine the result with a result from another async task. This is where CompletableFutures come in handy because they allow to express dependencies between tasks declaratively.
The name CompletableFuture comes from the fact that it is a Future which can be explicitly completed by calling the
complete() method. Instead of using an ExecutorService to return a Future, completing a CompletableFuture can be done explicitly in a thread.
As in the previous example with a Future, let’s say we’re making a network call to retrieve some data.
The computation (in this case a simulated network call) is done in a separate thread. A lambda expression is used to define the Runnable passed to the thread. Before returning a Future, the thread is started. The
getResultOverNetwork() method returns almost instantaneously, leaving the calling thread free to to other useful work.
Technically this example is almost the same as shown previously with a regular Future. The only difference is that instead of a plain old Thread, an ExecutorService was used. To be fair though, CompletableFuture class has some static methods which make the usage a little bit easier as can be seen in the following example.
But still, what’s so special about CompletableFutures?
CompletableFuture allows to compose multiple synchronous and/or asynchronous operations into a pipeline. So instead of calling
get() and blocking until a Future returns a value, it is possible to declaratively tell how to react to a value when it becomes available.
Although the example is trivial, it shows that the returned value from the first task is pipelined to the next. For example, in a real world scenario this could be used when you need to retrieve some data using a RESTful API and process it immediately afterwards.
In addition to
thenApply(), the API provides other useful methods.
thenCombine() can be used to combine the results from 2 tasks and
thenAccept() consumes the result from a task.
getVeggies() are implemented as follows.
In this example, 2 CompletableFutures are created using the
supplyAsync() static method. They’re pipelined using
thenCombine() which accepts a lambda expression as a second argument describing how to combine the results of the 2 tasks. These tasks are run in parallel and as soon as both become available their results are combined and then the end result is consumed using
thenAccept() (in this case consumption is done by printing out the members of the array). Method references
itemService::getVeggies simulate network calls and they return an array of strings.
The returned Future is of type
thenAccept() consumes the array. Therefore there’s no need to call
get() (although it is possible) and calling
join() will suffice.
join()- Returns the result value when complete, or throws an (unchecked) exception if completed exceptionally.
Pipelining methods come with asynchronous versions as well. Meaning that the next step in the pipeline will be executed in a new thread. For example, the API provides
thenAcceptAsync() which is an asynchronous version of
This is not by any means a full overview of the CompletableFuture API. For example, I did not cover exception handling. Be sure to have a look at the JavaDoc for all the possibilities or dive into a book for more detailed and thorough explanation.comments powered by Disqus