JavaMultithreading

Callable and Future

The Callable interface

Sometimes you need not only to execute a task in an executor but also to return a result of this task to the calling code. It is absolutely possible but inconvenient to do with Runnable's.

In order to simplify it, an executor supports another class of tasks named Callable that returns a result and may throw an exception. This interface belongs to the java.util.concurrent package. Let's take a look at this.

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
} 

As you can see, it is a generic interface where the type parameter V determines the type of a result. Since it is a functional interface, we can use it together with lambda expressions and method references as well as implementing classes.

Here is a Callable that emulates a long-running task and returns a number that was "calculated".

Callable<Integer> generator = () -> {
    TimeUnit.SECONDS.sleep(5);
    return 700000;
};

The same code can be overwritten using inheritance that is more boilerplate than the lambda.

Submitting a Callable and obtaining a Future

When we submit a Callable to an executor service, it cannot return a result directly since the submit method does not wait until the task completes. Instead, an executor returns a special object called Future that wraps the actual result which may not even exist yet. This object represents the result of an asynchronous computation (task).

ExecutorService executor = Executors.newSingleThreadExecutor();

Future<Integer> future = executor.submit(() -> {
    TimeUnit.SECONDS.sleep(5);
    return 700000;
};

Until the task completes, the actual result is not present in the future. To check it, there is a method named isDone(). Most likely, it will return false if you will call it immediately after obtaining a new future.

System.out.println(future.isDone()); // most likely it is false

Getting the actual result of a task

The result can only be retrieved from a future using the get method.

int result = future.get();

It returns the result when the computation has completed, or block the current thread and waits for the result. This method may throw two checked exceptions ExecutionException and InterruptedException which we omit for brevity.

If a submitted task executes an infinite loop or waits for an external resource too long time, a thread that invokes get will be blocked all this time. To prevent it, there is also an overloaded version of get with a waiting timeout.

int result = future.get(10, TimeUnit.SECONDS); // it blocks the current thread 

In this case, the calling thread waits for at most 10 seconds for the computation to complete. If the timeout ends, the method throws TimeoutException.

Cancellation a task

The Future class provides an instance method named cancel that attempts to cancel execution of the task. This method is more complicated than it might seem at the first look.

An attempt will fail if the task has already completed, has already been canceled or could not be canceled for some other reason. If successful, and this task has not started when the method is invoked, it will never run.

The method takes a boolean parameter which determines whether the thread executing this task should be interrupted in an attempt to stop the task (in other words, whether to stop already running task).

future1.cancel(true);  // try to cancel even if the task is executing now
future2.cancel(false); // try to cancel only if the task is not executing

Since passing true involves interruptions, the cancelation of an executing task is guaranteed only if it correctly handles InterruptedException and checks the flag Thread.currentThread().isInterrupted().

If a someone invokes future.get() at a successfully canceled task, the method throws an unchecked CancellationException. If you do not need to deal with it, you may check whether a task was canceled invoking isCancelled().

The advantage of using Callable and Future

The approach we are learning allows us to do something useful between obtaining a Future and getting the actual result. In this time interval, we can submit several tasks to an executor, and only after that wait for all results to aggregate them.

ExecutorService executor = Executors.newFixedThreadPool(4);

Future<Integer> future1 = executor.submit(() -> {
    TimeUnit.SECONDS.sleep(5);
    return 700000;
});

Future<Integer> future2 = executor.submit(() -> {
    TimeUnit.SECONDS.sleep(5);
    return 900000;
});

int result = future1.get() + future2.get(); // waiting for both results

System.out.println(result); // 1600000

If you have a modern computer, these tasks may be executed in parallel.

Methods invokeAll and invokeAny

In addition to all features described above, there are two useful methods for submitting batches of Future to an executor.

  • invokeAll accepts a prepared collection of callables and returns a collection of futures;
  • invokeAny also accepts a collection of callables and returns the result (not a future!) of one that has completed successfully.

Both methods also have overloaded versions that accept a timeout of execution that is often necessary in practice.

Suppose we need to calculate several numbers in separated tasks and them sum up the numbers in the main thread. It is simply to do using invokeAll method.

ExecutorService executor = Executors.newFixedThreadPool(4);
List<Callable<Integer>> callables =
        List.of(() -> 1000, () -> 2000, () -> 1500); // three "difficult" tasks

List<Future<Integer>> futures = executor.invokeAll(callables);
int sum = 0;
for (Future<Integer> future : futures) {
   sum += future.get(); // blocks on each future to get a result
}
System.out.println(sum);

Note, if your version of Java is 8 rather than 9+, just replace List.of(...) with Arrays.asList(...). If you know Stream API (Java 8) and would to practice it, you may rewrite this code using it.

Summary

Let's summarize the information about Callable and Future.

To get a result of an asynchronous task executed in ExecutorService you need to do three steps:

  1. create an object representing a Callable task;
  2. submit the task in ExecutorService and obtain a Future;
  3. invoke get to receive the actual result when you need it.
Using Future allows us do not block the current thread until we really want to receive a result of a task. It is also possible to start multiple tasks and then get all results to aggregate them in the current thread. In addition to making your program more response, it will speed up your calculations if your computer supports parallel execution of threads.

Also, you may use methods isDone, cancel and isCancelled of a future. But be careful with exception handling when using them. Unfortunately, we cannot give all possible receipts and best practices within the lesson, but they will appear with experience. The main thing is to read the documentation, especially in multi-threaded programming.
How did you like the theory?
Report a typo