Skip to main content

Java Executor Framework

Java Executor Framework is a built-in concurrency framework in Java that provides a powerful and flexible way to manage and execute tasks in parallel. It provides a set of interfaces and classes that allow you to decouple the execution of tasks from the mechanisms that implement them.

The main purpose of the Executor framework is to decouple the execution of tasks from the thread management mechanisms, allowing for better scalability, flexibility, and resource utilization.

Before the introduction of the Executor framework, Java provided low-level APIs for concurrency such as Thread and Runnable, which required developers to manage thread pools, synchronization, and other low-level details manually. This approach was error-prone and often led to inefficient or unsafe code.

The Executor framework provides a simple and standardized way to manage thread pools and execute tasks asynchronously, making it easier for developers to write safe and efficient concurrent code. It also provides additional features such as scheduling, timeouts, and cancellations, which are not available in the low-level APIs.

Some benefits of using the Executor framework include:

  • Simplified thread management: The Executor framework provides a high-level abstraction for managing thread pools and executing tasks, which simplifies the code and reduces the risk of errors.

  • Scalability: By decoupling the execution of tasks from the thread management mechanisms, the Executor framework allows for better scalability and resource utilization.

  • Improved performance: The Executor framework provides several optimizations such as thread pooling, work stealing, and task batching, which can improve the performance of concurrent applications.

  • Standardization: The Executor framework provides a standardized API for concurrency, which makes it easier to write portable and reusable code.

The Executor Framework consists of three main components: Executor, ExecutorService, and ThreadPoolExecutor.

1. Executor

The Executor interface provides a simple way to execute tasks asynchronously. It has a single method called execute(Runnable task), which takes a Runnable object and executes it asynchronously. Executors are usually created using the Executors factory class.

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

public class ExecutorExample {
public static void main(String[] args) {
// Create an Executor with a fixed thread pool of 3 threads
Executor executor = Executors.newFixedThreadPool(3);

// Submit some tasks for execution
executor.execute(() -> {
System.out.println("Task 1 executed by thread " + Thread.currentThread().getName());
});
executor.execute(() -> {
System.out.println("Task 2 executed by thread " + Thread.currentThread().getName());
});

}
}

2. ExecutorService

The ExecutorService interface extends the Executor interface and provides additional methods to manage the execution of tasks. It allows you to submit tasks for execution, retrieve the results of completed tasks, and manage the lifecycle of the executor service. Executors are usually created using the Executors factory class.

3. ThreadPoolExecutor

The ThreadPoolExecutor class is a concrete implementation of the ExecutorService interface that manages a pool of worker threads. It allows you to specify the size of the thread pool, the queue for holding tasks, and various other parameters that affect the behavior of the executor service.

Other key concepts in the Java Executor Framework include:

  • Callable: An interface similar to Runnable, but allows a task to return a value and throw exceptions.

  • Future: An interface that represents the result of an asynchronous computation. It allows you to check if the computation is complete, retrieve the result or exception, and cancel the computation.

  • CompletionService: An interface that combines the functionality of an Executor and a BlockingQueue. It allows you to submit tasks for execution and retrieve the results in the order they complete.

  • ExecutorCompletionService: A concrete implementation of the CompletionService interface that uses a ThreadPoolExecutor to execute tasks.

Assigning tasks to executor service

ExecutorService can execute Runnable and Callable tasks.

Runnable runnableTask = () -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
};

Callable<String> callableTask = () -> {
TimeUnit.MILLISECONDS.sleep(300);
return "Task's execution";
};

List<Callable<String>> callableTasks = new ArrayList<>();
callableTasks.add(callableTask);
callableTasks.add(callableTask);
callableTasks.add(callableTask);

We can assign tasks to the ExecutorService using several methods including execute(), which is inherited from the Executor interface, and also submit(), invokeAny() and invokeAll().

The execute() method is void and doesn't give any possibility to get the result of a task's execution or to check the task's status (is it running):

executorService.execute(runnableTask);

submit() submits a Callable or a Runnable task to an ExecutorService and returns a result of type Future:

Future<String> future = 
executorService.submit(callableTask);

invokeAny() assigns a collection of tasks to an ExecutorService, causing each to run, and returns the result of a successful execution of one task (if there was a successful execution):

String result = executorService.invokeAny(callableTasks);

invokeAll() assigns a collection of tasks to an ExecutorService, causing each to run, and returns the result of all task executions in the form of a list of objects of type Future:

List<Future<String>> futures = executorService.invokeAll(callableTasks);

Shtting down an Executor Service

ExecutorService will not be automatically destroyed when there is no task to process. It will stay alive and wait for new work to do. this is very helpful, such as when an app needs to process tasks that appear on an irregular basis or the task quantity is not known at compile time.

On the other hand, an app could reach its end but not be stopped because a waiting ExecutorService will cause the JVM to keep running.

To properly shut down an ExecutorService, we have the shutdown() and shutdownNow() APIs.

The shutdown() method doesn't cause immediate destruction of the ExecutorService. It will make the ExecutorService stop accepting new tasks and shut down after all running threads finish their current work:

executorService.shutdown();

The shutdownNow() method tries to destroy the ExecutorService immediately, but it doesn't guarantee that all the running threads will be stopped at the same time:

List<Runnable> notExecutedTasks = executorService.shutDownNow();

The submit() and invokeAll() methods return an object or a collection of objects of type Future, which allows us to get the result of a task's execution or to check the task's status (is it running).

Future Interface

The Future interface provides a special blocking method get(), which returns an actual result of the Callable task's execution or null in the case of a Runnable task:

Future<String> future = executorService.submit(callableTask);
String result = null;
try {
result = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}

Calling the get() method while the task is still running will cause execution to block until the task properly executes and the result is available.

With very long blocking caused by the get() method, an application's performance can degrade. If the resulting data is not crucial, it is possible to avoid such a problem by using timeouts:

String result = future.get(200, TimeUnit.MILLISECONDS);

If the execution period is longer than specified (in this case, 200 milliseconds), a TimeoutException will be thrown.

We can use the isDone() method to check if the assigned task already processed or not.

The Future interface also provides for canceling task execution with the cancel() method and checking the cancellation with the isCancelled() method:

boolean canceled = future.cancel(true);
boolean isCancelled = future.isCancelled();