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.
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(...) |
+-----------------------------------------------------------+
Executor: The foundational root interface providing a single method:void execute(Runnable command). It models a fire-and-forget submission model.ExecutorService: ExtendsExecutor, adding robust lifecycle management (shutdown,shutdownNow), task submission returningFutureinstances, and batch execution capabilities (invokeAll,invokeAny).ScheduledExecutorService: ExtendsExecutorServiceto 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 Method | Core / Max Threads | Work Queue Type | Idle Thread Timeout | Common Use Case |
|---|---|---|---|---|
newSingleThreadExecutor() | 1 / 1 | Unbounded LinkedBlockingQueue | N/A | Strictly sequential background execution |
newFixedThreadPool(n) | n / n | Unbounded LinkedBlockingQueue | None (retained) | Predictable, CPU-bounded resource limits |
newCachedThreadPool() | 0 / Integer.MAX_VALUE | SynchronousQueue (direct handoff) | 60 seconds | High-volume, short-lived asynchronous tasks |
newScheduledThreadPool(n) | n / Integer.MAX_VALUE | Delayed work queue | None (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:
execute(Runnable): Defined inExecutor. It returnsvoid. If the task throws an uncaught runtime exception, it terminates the executing worker thread and propagates to the thread'sUncaughtExceptionHandler.submit(Runnable): Defined inExecutorService. It returns aFuture<?>. Upon successful completion, callingfuture.get()returnsnull. Any uncaught exception thrown during task execution is captured and re-thrown inside anExecutionExceptionwhenget()is called.submit(Callable<T>): Returns aFuture<T>. When the task completes,future.get()yields the returned value of typeT.submit(Runnable, T result): Returns aFuture<T>that produces the predeterminedresultobject 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. ThrowsInterruptedException(if the waiting thread is interrupted) andExecutionException(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 throwsTimeoutExceptionwithout cancelling the task.boolean isDone(): Non-blocking query returningtrueif the task completed normally, threw an exception, or was cancelled.boolean isCancelled(): Returnstrueif the task was cancelled before completing.boolean cancel(boolean mayInterruptIfRunning): Attempts to abort execution. IfmayInterruptIfRunningistrue, the thread executing the task is sent aninterrupt()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 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 and the commencement of execution . 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 tosubmit()orexecute()throwsRejectedExecutionException. Already accepted tasks (both active and waiting in queue) continue to execution.shutdownNow(): Attempts to stop actively executing tasks viaThread.interrupt(), drains all awaiting tasks from the work queue, and returns them as aList<Runnable>.isShutdown(): Returnstrueimmediately aftershutdown()orshutdownNow()has been invoked.isTerminated(): Returnstrueonly after all tasks have completed and all worker threads have stopped.
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?
Each subsequent execution begins immediately after the preceding execution finishes, running every 3 seconds without overlapping.
The scheduler buffers the executions and executes them in a batch after 6 seconds.
The scheduler throws a TaskOverrunException because the execution duration exceeds the period.
The scheduler spawns a second thread to run executions concurrently every 2 seconds.
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?
RuntimeException -> IllegalStateException
IllegalStateException -> null
NullPointerException -> IllegalStateException
ExecutionException -> IllegalStateException
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?
It prints 'true false ' and throws RejectedExecutionException upon submitting the second task.
It prints 'false false ' and prints 'Extra' before terminating.
It prints 'true true ' and successfully queues the second task.
It throws IllegalStateException on executor.shutdown() because active tasks exist.
Sections you finish are checked off in the contents.