13.3 The Concurrency Framework: `ExecutorService`, Thread Pools, and `Future`

Key Takeaways

  • The Executor framework decouples task submission from execution mechanics, eliminating thread creation overhead and thread exhaustion through managed pools.

  • The Executors utility class supplies standard pool configurations, including newFixedThreadPool, newCachedThreadPool, newSingleThreadExecutor, and newScheduledThreadPool.

  • Tasks submitted via execute() run fire-and-forget returning void, whereas submit() accepts Runnable or Callable and returns a Future to monitor and control task execution.

  • Calling Future.get() blocks the calling thread until computation completes and wraps task exceptions inside an ExecutionException.

  • shutdown() ceases acceptance of new tasks while completing existing work, whereas shutdownNow() actively interrupts running threads and drains pending tasks from the queue.

Last updated: October 2026

The Concurrency Framework: ExecutorService, Thread Pools, and Future

Manually creating and managing Thread instances in production applications creates severe architectural bottlenecks. Each thread allocation incurs operating system memory overhead and context-switching penalties. Unbounded thread instantiation under heavy load leads to resource exhaustion and OutOfMemoryError: unable to create new native thread. Java solves this through the Executor Framework in java.util.concurrent, which decouples task specification from thread management and provides thread pooling, lifecycle control, and asynchronous result retrieval.


The Executor Hierarchy

The framework is structured across three core interfaces:

+-----------------------------------------------------------+
|                    Executor                               |
|   void execute(Runnable command)                          |
+-----------------------------------------------------------+
                              ^
                              |
+-----------------------------------------------------------+
|                 ExecutorService                           |
|   <T> Future<T> submit(Callable<T> task)                  |
|   Future<?> submit(Runnable task)                         |
|   void shutdown() / List<Runnable> shutdownNow()          |
|   List<Future<T>> invokeAll(Collection<Callable<T>> tasks)|
|   T invokeAny(Collection<Callable<T>> tasks)              |
+-----------------------------------------------------------+
                              ^
                              |
+-----------------------------------------------------------+
|             ScheduledExecutorService                      |
|   schedule(...)                                           |
|   scheduleAtFixedRate(...)                                |
|   scheduleWithFixedDelay(...)                             |
+-----------------------------------------------------------+
  1. Executor: The foundational root interface providing a single method: void execute(Runnable command). It models a fire-and-forget submission model.
  2. ExecutorService: Extends Executor, adding robust lifecycle management (shutdown, shutdownNow), task submission returning Future instances, and batch execution capabilities (invokeAll, invokeAny).
  3. ScheduledExecutorService: Extends ExecutorService to support one-shot delayed execution and periodic recurring task execution.

Thread Pool Factories in Executors

The java.util.concurrent.Executors utility class provides static factory methods for creating pre-configured thread pools:

ExecutorService single = Executors.newSingleThreadExecutor();
ExecutorService fixed  = Executors.newFixedThreadPool(4);
ExecutorService cached = Executors.newCachedThreadPool();
ScheduledExecutorService sched = Executors.newScheduledThreadPool(2);
Factory MethodCore / Max ThreadsWork Queue TypeIdle Thread TimeoutCommon Use Case
newSingleThreadExecutor()1 / 1Unbounded LinkedBlockingQueueN/AStrictly sequential background execution
newFixedThreadPool(n)n / nUnbounded LinkedBlockingQueueNone (retained)Predictable, CPU-bounded resource limits
newCachedThreadPool()0 / Integer.MAX_VALUESynchronousQueue (direct handoff)60 secondsHigh-volume, short-lived asynchronous tasks
newScheduledThreadPool(n)n / Integer.MAX_VALUEDelayed work queueNone (for core)Recurring maintenance and polling schedules

Note

newSingleThreadExecutor() guarantees that if the single worker thread terminates abruptly due to a failure during task execution, a new worker thread is automatically created to replace it for subsequent tasks.


Submitting Tasks: execute() vs. submit()

An ExecutorService accepts tasks through several methods:

  1. execute(Runnable): Defined in Executor. It returns void. If the task throws an uncaught runtime exception, it terminates the executing worker thread and propagates to the thread's UncaughtExceptionHandler.
  2. submit(Runnable): Defined in ExecutorService. It returns a Future<?>. Upon successful completion, calling future.get() returns null. Any uncaught exception thrown during task execution is captured and re-thrown inside an ExecutionException when get() is called.
  3. submit(Callable<T>): Returns a Future<T>. When the task completes, future.get() yields the returned value of type T.
  4. submit(Runnable, T result): Returns a Future<T> that produces the predetermined result object upon task completion.

Managing Asynchronous Results with Future<T>

A Future<T> represents the lifecycle and deferred result of an asynchronous computation:

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
    Thread.sleep(1000);
    return 42;
});

System.out.println("Is completed? " + future.isDone()); // false
Integer result = future.get(); // BLOCKS until calculation finishes!
System.out.println("Calculated: " + result); // 42
executor.shutdown();

Core Future API Methods

  • V get(): Blocks the calling thread indefinitely until the task completes. Throws InterruptedException (if the waiting thread is interrupted) and ExecutionException (if the underlying computation threw an exception).
  • V get(long timeout, TimeUnit unit): Blocks up to the specified duration. If the task does not finish within that window, it throws TimeoutException without cancelling the task.
  • boolean isDone(): Non-blocking query returning true if the task completed normally, threw an exception, or was cancelled.
  • boolean isCancelled(): Returns true if the task was cancelled before completing.
  • boolean cancel(boolean mayInterruptIfRunning): Attempts to abort execution. If mayInterruptIfRunning is true, the thread executing the task is sent an interrupt() request.

Batch Task Execution: invokeAll() vs. invokeAny()

ExecutorService simplifies multi-task orchestration through two batch methods:

invokeAll()

Executes a collection of Callable tasks, blocking until all tasks complete (either normally or exceptionally). It returns a List<Future<T>> in the exact iteration order of the original task collection. Every returned Future is guaranteed to have isDone() == true.

invokeAny()

Executes a collection of Callable tasks and blocks until any one task completes successfully (without throwing an exception). It returns the value of that first successful task and cancels all remaining uncompleted tasks. If all tasks fail, an ExecutionException is thrown.


Scheduling Tasks with ScheduledExecutorService

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

1. schedule(Callable/Runnable, long delay, TimeUnit unit)

Executes a task once after the specified delay elapses.

2. scheduleAtFixedRate(command, initialDelay, period, unit)

Schedules periodic execution where the target start time of execution n+1n+1 is calculated strictly as initialDelay + (n * period). If a task execution takes longer than period, the subsequent execution begins immediately upon completion—executions never run concurrently on the same schedule, but execution times will not drift.

3. scheduleWithFixedDelay(command, initialDelay, delay, unit)

Schedules periodic execution where a fixed delay occurs between the completion of execution nn and the commencement of execution n+1n+1. The interval between executions is always executionDuration + delay.

Warning

Periodic Exception Suppression If any execution of a periodic task in scheduleAtFixedRate or scheduleWithFixedDelay throws an uncaught exception, subsequent executions are silently suppressed and cancelled. Always wrap periodic task logic in robust try-catch blocks.


Graceful Shutdown and Lifecycle States

An ExecutorService does not terminate automatically when tasks finish; its worker threads remain active, preventing JVM shutdown unless closed explicitly.

ExecutorService executor = Executors.newFixedThreadPool(2);
// submit tasks...

executor.shutdown(); // Reject new submissions, finish existing tasks
try {
    if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
        List<Runnable> droppedTasks = executor.shutdownNow(); // Force cancel
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
    Thread.currentThread().interrupt();
}
  • shutdown(): Stops accepting new tasks. Any subsequent call to submit() or execute() throws RejectedExecutionException. Already accepted tasks (both active and waiting in queue) continue to execution.
  • shutdownNow(): Attempts to stop actively executing tasks via Thread.interrupt(), drains all awaiting tasks from the work queue, and returns them as a List<Runnable>.
  • isShutdown(): Returns true immediately after shutdown() or shutdownNow() has been invoked.
  • isTerminated(): Returns true only after all tasks have completed and all worker threads have stopped.
Test Your Knowledge

A developer configures a ScheduledExecutorService with a single worker thread and schedules a task using scheduleAtFixedRate with an initial delay of 0 seconds and a period of 2 seconds. The task requires 3 seconds to complete each execution. How does the scheduler handle consecutive executions?

A

Each subsequent execution begins immediately after the preceding execution finishes, running every 3 seconds without overlapping.

B

The scheduler buffers the executions and executes them in a batch after 6 seconds.

C

The scheduler throws a TaskOverrunException because the execution duration exceeds the period.

D

The scheduler spawns a second thread to run executions concurrently every 2 seconds.

Test Your Knowledge

A task submitted to an ExecutorService throws an unhandled RuntimeException during execution:

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
    throw new IllegalStateException("Database unreachable");
});
try {
    String value = future.get();
} catch (Exception e) {
    System.out.println(e.getClass().getSimpleName() + " -> " + e.getCause().getClass().getSimpleName());
}
finally {
    executor.shutdown();
}

What is printed to the console?

A

RuntimeException -> IllegalStateException

B

IllegalStateException -> null

C

NullPointerException -> IllegalStateException

D

ExecutionException -> IllegalStateException

Test Your Knowledge

Consider the following code evaluating ExecutorService shutdown behavior:

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
    try { Thread.sleep(500); } catch (InterruptedException e) {}
});
executor.shutdown();
System.out.print(executor.isShutdown() + " " + executor.isTerminated() + " ");
executor.submit(() -> System.out.print("Extra"));

What occurs when this code executes?

A

It prints 'true false ' and throws RejectedExecutionException upon submitting the second task.

B

It prints 'false false ' and prints 'Extra' before terminating.

C

It prints 'true true ' and successfully queues the second task.

D

It throws IllegalStateException on executor.shutdown() because active tasks exist.

Sections you finish are checked off in the contents.