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
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.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
true
involves interruptions, the cancelation of an executing task is guaranteed only if it correctly handles InterruptedException
and checks the flag Thread.currentThread().isInterrupted()
.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:
- create an object representing a
Callable
task; - submit the task in
ExecutorService
and obtain aFuture
; - invoke
get
to receive the actual result when you need it.
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.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.