Guide to the Future Interface in Java

Introduction

In this article, we will overview the functionality of the Future interface as one of Java's concurrency constructs. We'll also look at several ways to create an asynchronous task, because a Future is just a way to represent the result of an asynchronous computation.

The java.util.concurrent package was added to Java 5. This package contains a set of classes that makes the development of concurrent applications in Java easier. In general, concurrency is a fairly complex subject and it may seem a bit daunting.

A Java Future is very similar to a JavaScript Promise.

Motivation

A common task for asynchronous code is providing a responsive UI in an application running a costly computation or data reading/writing operation.

Having a frozen screen or no indication that the process is in progress results in fairly bad user experience. The same goes for applications that are outright slow:

Minimization of idle time by switching tasks can improve the performance of an application drastically, though it depends on what kind of operations are involved.

Fetching a web resource can be delayed or slow in general. Reading a huge file can be slow. Waiting for a result of cascading microservices can be slow. In synchronous architectures, the application waiting for the result waits for all of these processes to complete before proceeding.

In asynchronous architectures, it continues doing things that it can without the returned result in the meantime.

Implementation

Before starting with examples, let's look at the basic interfaces and classes from the java.util.concurrent package that we are going to use.

The Java Callable interface is an improved version of Runnable. It represents a task that returns a result and may throw an exception. To implement Callable, you have to implement the call() method with no arguments.

To submit our Callable for concurrent execution, we'll use the ExecutorService. The easiest way to create an ExecutorService is to use one of the factory methods of the Executors class. After the creation of the asynchronous task, a Java Future object is returned from the executor.

If you'd like to read more about The Executor Framework, we've got an in-depth article on that.

The Future Interface

The Future interface is an interface that represents a result that will eventually be returned in the future. We can check if a Future has been fed the result, if it's awaiting a result or if it has failed before we try to access it, which we'll cover in the upcoming sections.

Let's first take a look at the interface definition:

public interface Future<V> {
	V get() throws InterruptedException, ExecutionException;
	V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
	boolean isCancelled();
	boolean isDone();
	boolean cancel(boolean mayInterruptIfRunning)
}

The get() method retrieves the result. If the result has not yet been returned into a Future instance, the get() method will wait for the result to be returned. It's crucial to note that get() will block your application if you call it before the result has been returned.

You can also specify a timeout after which the get() method will throw an exception if the result hasn't yet been returned, preventing huge bottlenecks.

The cancel() method attempts to cancel the execution of the current task. The attempt will fail if the task has already completed, has been cancelled, or could not be cancelled because of some other reasons.

The isDone() and isCancelled() methods are dedicated to finding out the current status of an associated Callable task. You'll typically use these as conditionals to check if it makes sense to use the get() or cancel() methods.

The Callable Interface

Let's create a task that takes some time to complete. We'll define a DataReader that implements Callable:

public class DataReader implements Callable {
    @Override
    public String call() throws Exception {
        System.out.println("Reading data...");
        TimeUnit.SECONDS.sleep(5);
        return "Data reading finished";
    }
}

To simulate a costly operation, we're using TimeUnit.SECONDS.sleep(). It calls Thread.sleep(), but is a bit cleaner for longer periods of time.

Similarly, let's have a processor class that processes some other data at the same time:

public class DataProcessor implements Callable {
    @Override
    public String call() throws Exception {
        System.out.println("Processing data...");
        TimeUnit.SECONDS.sleep(5);
        return "Data is processed";
    }
}

Both of these methods take 5 seconds each to run. If we were to simply call one after another synchronously, reading and processing would take ~10s.

Running Future Tasks

Now, to call these methods from another, we'll instantiate an executor and submit our DataReader and DataProcessor to it. The executor returns a Future, so we'll pack the result of it into a Future-wrapped object:

public static void main(String[] args) throws InterruptedException, ExecutionException {
    ExecutorService executorService = Executors.newFixedThreadPool(2);

    Future<String> dataReadFuture = executorService.submit(new DataReader());
    Future<String> dataProcessFuture = executorService.submit(new DataProcessor());

    while (!dataReadFuture.isDone() && !dataProcessFuture.isDone()) {
            System.out.println("Reading and processing not yet finished.");
            // Do some other things that don't depend on these two processes
            // Simulating another task
            TimeUnit.SECONDS.sleep(1);
        }
    System.out.println(dataReadFuture.get());
    System.out.println(dataProcessFuture.get());
}

Here, we've created an executor with two threads in the pool since we have two tasks. You can use the newSingularThreadExecutor() to create a single one if you only have one concurrent task to execute.

If we submit more than these two tasks into a this pool, the additional tasks will wait in the queue until a free spot emerges.

Running this piece of code will yield:

Reading and processing not yet finished.
Reading data...
Processing data...
Reading and processing not yet finished.
Reading and processing not yet finished.
Reading and processing not yet finished.
Reading and processing not yet finished.
Data reading finished
Data is processed

The total runtime will be ~5s, not ~10s since both of these were concurrently running at the same time. As soon as we've submitted the classes to the executor, their call() methods have been called. Even having a Thread.sleep() of one second five times doesn't affect the performance much since it's running on its own thread.

It's important to note that the code didn't run any faster, it just didn't wait redundantly for something it didn't have to and performed other tasks in the meantime.

What's important here is the usage of the isDone() method. If we didn't have the check, there wouldn't be any guarantee that the results were packed in the Futures before we've accessed them. If they weren't, the get() methods would block the application until they did have results.

Future Timeout

If there were no checks for the completion of future tasks:

public static void main(String[] args) throws InterruptedException, ExecutionException {
    ExecutorService executorService = Executors.newFixedThreadPool(2);

    Future<String> dataReadFuture = executorService.submit(new DataReader());
    Future<String> dataProcessFuture = executorService.submit(new DataProcessor());

    System.out.println("Doing another task in anticipation of the results.");
    // Simulating another task
    TimeUnit.SECONDS.sleep(1);
    System.out.println(dataReadFuture.get());
    System.out.println(dataProcessFuture.get());
}

The execution time would still be ~5s, though, we'd be faced with a big issue. It takes 1 second to complete an additional task, and 5 to complete the other two.

Sounds just like last time?

4 out of 5 seconds in this program are blocking. We've tried getting the result of the future before it was returned and have blocking 4 seconds until they do return.

Let's set a constraint for getting these methods. If they don't return within a certain expected timeframe, they'll throw exceptions:

String dataReadResult = null;
String dataProcessResult = null;

try {
    dataReadResult = dataReadFuture.get(4, TimeUnit.SECONDS);
    dataProcessResult = dataProcessFuture.get(0, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
    e.printStackTrace();
}

System.out.println(dataReadResult);
System.out.println(dataProcessResult);

Both these take 5s each. With a head start wait of a second from the other task, the dataReadFuture is returned within an additional 4 seconds. The data process result is returned at the same time and this code runs well.

If we gave it an unrealistic time to execute (less than 5s total), we'd be greeted with:

Reading data...
Doing another task in anticipation of the results.
Processing data...
java.util.concurrent.TimeoutException
	at java.util.concurrent.FutureTask.get(FutureTask.java:205)
	at FutureTutorial.Main.main(Main.java:21)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
null
null

Of course, we wouldn't simply print the stack trace in an actual application, but rather reroute the logic to handle the exceptional state.

Cancelling Futures

In some cases, you might want to cancel a future. For example, if you don't receive a result within n seconds, you might just decide to not use the result at all. In that case, there's no need to have a thread still execute and pack the result since you won't be using it.

This way, you free up a space for another task in the queue or simply release the resources allocated to an unnecessary costly operation:

boolean cancelled = false;
if (dataReadFuture.isDone()) {
    try {
        dataReadResult = dataReadFuture.get();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
} else {
cancelled = dataReadFuture.cancel(true);
}
if (!cancelled) {
    System.out.println(dataReadResult);
} else {
    System.out.println("Task was cancelled.");
}

If the task had been done, we get the result and pack it in our result String. Otherwise, we cancel() it. If it wasn't cancelled, we print the value of the resulting String. By contrast, we notify the user that the task was cancelled otherwise.

What's worth noting is that the cancel() method accepts a boolean parameter. This boolean defines whether we allow the cancel() method to interrupt the task execution or not. If we set it as false, there's a possibility that the task won't be cancelled.

We have to assign the return value of the cancel() method to a boolean as well. The returned value signifies if the method ran successfully or not. If it fails to cancel a task, the boolean will be set as false.

Running this code will yield:

Reading data...
Processing data...
Task was cancelled.

And if we try getting the data from a cancelled task, a CancellationException is generated:

if (dataReadFuture.cancel(true)) {
    dataReadFuture.get();
}

Running this code will yield:

Processing data...
Exception in thread "main" java.util.concurrent.CancellationException
	at java.util.concurrent.FutureTask.report(FutureTask.java:121)
	at java.util.concurrent.FutureTask.get(FutureTask.java:192)
	at FutureTutorial.Main.main(Main.java:34)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

Limitations of the Future

The Java Future was a good step towards asynchronous programming. But, as you already may have notices, it's rudimentary:

  • Futures can not be explicitly completed (setting its value and status).
  • It doesn't have a mechanism to create stages of processing that are chained together.
  • There is no mechanism to run Futures in parallel and after to combined their results together.
  • The Future does not have any exception handling constructs.

Fortunately, Java provides concrete Future implementations that provide these features (CompletableFuture, CountedCompleter, ForkJoinTask, FutureTask, etc).

Conclusion

When you need to wait for another process to complete without blocking, it can be useful to go asynchronous. This approach helps to improve the usability and performance of applications.

Java includes specific constructs for concurrency. The basic one is the Java Future that represents the result of asynchronous computation and provides basic methods for handling the process.

Author image
Lviv, Ukraine