This is part 7 of this series. If you missed any of the last six, you can find them here:
- Part 1 – Overview
- Part 2 – Lifecycle of threads
- Part 3 – Daemon threads
- Part 4 – Interrupting threads
- Part 5 – Thread synchronization
- Part 6 – Runnable vs Callable
Introduction
In the last post we revised our knowledge of the Runnable
interface, and then we introduced the Callable
interface. In this post we’ll look at how to create Executor
and ExecutorService
objects. We’ll also and how to run Callable
objects and access their return values using the Future
interface.
Executors and Executor Services
We know that we can pass a Runnable
object to a Thread
constructor, and then start the thread object. This means that we must manually control the lifecycle of a thread. It is error-prone and code-intensive.
The Concurrency API provides much easier ways to run threads. One of the main ways is by using concrete class implementations of the Executor
and ExecutorService
interfaces.
Let’s start with the Executor
interface.
public interface Executor {
public void execute(Runnable runnable);
}
The Executor
interface executes Runnable
tasks. Instead of explicitly creating threads, all we do is submit a Runnable
task to an Executor
implementation, and it does the rest. We don’t have to worry about the actual mechanics of how each task is run.
For example, usually we would create the following code for each task we want to run:
Thread thread1 = new Thread(new RunnableTask1());
thread1.start();
Thread thread2 = new Thread(new RunnableTask2());
thread2.start();
Using an Executor
we will just write the following code:
Executor executor = aConcreteExecutor; // created by a factory; see later
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
The ExecutorService
interface extends the functionality of the Executor
interface by adding a number of extra methods. Most of these methods are focused on running Callable
tasks, including:
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit);
<T> T invokeAny(Collection<? extends Callable<T>> tasks);
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit);
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
We can see that even though Runnable
tasks don’t return values, we can submit a Runnable
with an extra parameter of the desired result type and value.
We’ll use the submit()
methods after we learn how to create ExecutorService
objects.
Creating Executors and ExecutorServices
We can create Executor
and ExecutorService
objects by using the factory methods of the Executors
class. These static methods include overloaded versions of the following methods (all returning an ExecutorService
object):
newCachedThreadPool()
newFixedThreadPool()
newScheduledThreadPool()
newSingleThreadExecutor()
newSingleThreadScheduledExecutor()
newWorkStealingPool()
All of these factory methods can be called in a similar way to the following:
ExecutorService executorService = ExecutorService.newCachedThreadPool();
Then we can use the executorService
object to invoke collections of Callable
tasks and/or submit either Callable
or Runnable
tasks to it.
We can also create our own ExecutorService
instances by using the ForkJoinPool
, ThreadPoolExecutor
, and ScheduledThreadPoolExecutor
classes if we want more control over the creation process.
Submitting Callable Objects
Both Callable
and Runnable
objects can be submitted to executor services. Runnable
objects don’t return values, while Callable
objects do. However, we’ve already seen that we can submit a Runnable
task with a default return value. How do we access the returned values from these tasks?
Since the submit()
method executes the task asynchronously, it doesn’t block while we wait for the task to complete. The executor service can’t return the result of the Callable
directly. Instead the service returns a result of type Future
which can be used to retrieve the actual result at a later stage.
Let’s use the Callable
task from last week’s post as an example:
Callable<Integer> task = () -> {
try {
Thread.sleep(1000); // 1000 milliseconds
return 42;
}
catch (InterruptedException e) {
throw new IllegalStateException("Task interrupted!", e);
}
};
Now we can create an ExecutorService
object to run the Callable
task:
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);
System.out.println("Future done? " + future.isDone());
Integer result = future.get(); // blocks here until the Future is done
System.out.println("Future done? " + future.isDone());
System.out.println("Result of future = " + result);
In this code we submit the Callable
task. If we immediately check whether the Future
has finished,isDone()
will return false
because there’s a one second delay in the task. Calling the method get()
blocks the current thread and waits until the task has finished before returning the result. At this stage, calling isDone()
now returns true
.
The output will be the following:
Future done? false
Future done? true
Result = 42
Don’t worry if you don’t entirely understand the previous code. In the next post, we’ll cover Future
s in more detail. For the time being, think of a Future
as a placeholder variable that stands in for a future result of a computation. We can submit a task to an executor service, and pretty much forget about it until we need the result of the task. At that point all we have to do is to query the Future
with future.isDone()
to find out whether the task is done. If so, we can get the result by calling future.get()
. If not, we can either get busy with something else, or we can block at that point to wait for the task to complete.
Another thing we’ll look at next week is shutting down the executor service correctly. If you type in the previous code and run it, you’ll find that the program hangs and doesn’t terminate. You have to press Ctrl-C on Windows/Linux to terminate the JVM.
Has this series helped you? Do you have more questions about threads? Please share your thoughts and comments.
Until then, stay safe and keep learning!