Java8 Futures: Introduction & Best Practices

Reading Time: 3 minutes

Hi there! Today, we are going to talk about Futures in Java. We will also look at some of the best practices related to them.

What are Java Futures and why do we need them?

To understand this better, firstly we must understand what is blocking and why is it bad for our software.

BLOCKING – A blocking/long-running call occurs when a thread is tied up for long periods of time performing computations or waiting for resources. This may be anything – a database call, file I/O, serialization/deserialization of objects, network I/O etc.

There are multiple reasons why blocking negatively affects our code.

  1. During this period the threads, memory and other resources will not be released for use by other processes.
  2. Code following the blocking call will not be executed until a result from the blocked code arrives.
  3. Blocking is a potential bottleneck. It limits an application’s ability to scale.

But how do we solve the problem of blocking?

Blocking is inevitable in most systems. To maintain performance and scalability, we can isolate the blocking operations using Java Futures.

JAVA FUTURESThese allow us to isolate the blocking operations to a separate thread so that the execution of the main thread continues uninterrupted. The result of the futures is handled through a callback.

Futures represent the promise of value. In Java 8, the promise of values is represented by a CompletableFuture.

A CompletableFuture eventually resolves into one of the following 2 things –
1) The value of the future
2) An exception that occurs while resolving the value of the future.

Syntax of a CompletableFuture is as below-

CompletableFuture<Order> futureOrder = CompletableFuture.supplyAsync(() -> new Order(..));

This future takes in a lambda expression that takes some parameters. The lambda body contains all the work that is to be done in the blocking call. When the work is done, the future returns the value (in this case the order object) back to the calling code. All this work is done in a separate thread so that the normal flow of the program is not blocked.

Best Practices in Java 8 Futures:

Now, we will talk about some of the scenarios regarding futures & best practices we can leverage to our benefit.

  1. Using .get & .join to get the value of the future is not a good practice as these are blocking operations. These operations force the current thread to wait for the future to complete before moving on.

Best Practice: Rather than waiting for a future to complete, we can use transformations or callbacks to handle the result of the future. The .thenApply function transforms the value of the future using the lambda provided.

CompletableFuture<String> futureString = futureOrder.thenApply((order) -> order.toString());

2. Sometimes, if a lambda returns a future we can get a nested future.

Best Practice: We can use .thenCompose function to flatten the nested future instead of .thenApply. This returns a CompletableFuture<Order> instead of a CompletableFuture<CompletableFuture<Order>>.

CompletableFuture<Order> flattenedFutureOrder = futureOrder.thenCompose((order) -> completedFuture(order));

3. Futures are executed in separate threads or thread pool. Management of these threads is handled by an Executor or Executor Service. When no Executor is provided for a future, a default thread pool is used. This is convenient but also means that all operations, even the fast ones are competing for the same threads.

Best Practice: Isolate blocking operations to their own Executor. Leave the default Executor for faster operations. In the example below the future runs using a separate thread pool provided by the ExecutorService.

ExecutorService executorService = Executors.newFixedThreadPool(10);
CompletableFuture<Order> futureOrder = CompletableFuture.supplyAsync(() -> new Order(..), executor);

4. Since there many types of built-in executors, which one should we pick for which operation?

Best Practice: For blocking or long-running operations, a fixed thread pool is preferred. For example – database operations, HTTP connections etc. For fast, non-blocking operations we can use CachedThreadPool and WorkStealingPool as these won’t result in an ever-increasing number of threads.

In this blog, we learnt about Futures in Java 8 and some of the best practices involved while writing them. Keep learning!