The Executor Framework – Concurrency: Part II
23.2 The Executor Framework
Executors provide a high-level approach to launching tasks and managing threads. Tasks are executed asynchronously by threads in a multithreaded environment, where a task defines a unit of work—that is, a task is a set of instructions executed by a thread.
Internally, an executor maintains a thread pool that utilizes a number of reusable threads that are assigned tasks from a task queue, according to the execution policy of the executor. The threads maintained by a thread pool are also known as worker threads. A worker thread remains alive after executing a task, and can be assigned another task that is waiting to be executed.
Executors allow the submission of tasks for execution to be decoupled from the actual execution of tasks. The application defines and submits the tasks to the executor which manages their execution by assigning them to threads. The executor provides the necessary support for managing its lifecycle that comprises creating the executor, submitting tasks, managing the outcome of task execution, and shutting down the executor.
Figure 23.1 shows the interfaces in the java.util.concurrent package that define the characteristics of executors from basic to more advanced executors. The Executors utility class provides static methods that create various kinds of executors that not only implement the executor interfaces shown in Figure 23.1, but also implement various policies for task execution (Table 23.2, p. 1426).
Figure 23.1 Executor Interfaces in the java.util.concurrent Package
The Executor Interface
The java.util.concurrent.Executor interface defines a basic executor. This executor executes a Runnable task that is passed to its execute() method. Technically, the Executor interface is a functional interface as it has a single abstract method. However, it is not annotated as such in the Java SE Platform API documentation, as it is recommended to use the more versatile executors that extend the Executor interface (Figure 23.1).
The SimpleExecutor class below implements the Executor interface. Its execute() method shows two implementations, (1a) and (1b), for executing a Runnable task. Typically, an executor delegates the task to a new thread for asynchronous execution as shown at (1a). It can also be executed synchronously by calling the run() method on the task as shown at (1b).
class SimpleExecutor implements Executor {
@Override
public void execute(Runnable task) {
new Thread(task).start(); // (1a) Asynchronous call
// task.run(); // (1b) Synchronous call. Not recommended.
}
}
// Client code
SimpleExecutor executor = new SimpleExecutor();
Runnable task = () -> System.out.println(“Executing task …”);
executor.execute(task);
The essence of the basic executor is to replace the following code for running a task in a thread:
new Thread(task).start();
with the following code using an executor, where the actual execution is managed by the executor, abstracting away the management of the underlying thread:
executor.execute(task);
Typically, the tasks are executed by a thread pool maintained by the executors provided by the Executors utility class.
The following method is defined in the Executor interface:
void execute(Runnable task)
Executes the given task at some time in the future. The task may execute in a new thread, in a worker thread, or in the calling thread, at the discretion of the Executor implementation. Note that this method does not return a value and it does not specify any throws clause with checked exceptions.