9.3 Concurrency Utilities: ExecutorService, Callable, and Future
Key Takeaways
- The Executor framework decouples asynchronous task submission from thread management, execution policies, and scheduling through configurable thread pools.
- Callable<V> represents a value-returning task that can throw checked exceptions via V call(), whereas Runnable returns void and cannot throw checked exceptions.
- Future.get() synchronously blocks until computation completes, unwrapping task exceptions inside an ExecutionException accessible via getCause().
- ScheduledExecutorService distinguishes between scheduleAtFixedRate (fixed cadence between task initiations) and scheduleWithFixedDelay (fixed pause between task completion and subsequent start).
- In Java SE 21, ExecutorService implements AutoCloseable, allowing try-with-resources blocks to automatically initiate shutdown() and await task termination indefinitely.
Concurrency Utilities: ExecutorService, Callable, and Future
Manually creating and managing Thread instances for every background task introduces high memory overhead, unpredictable resource consumption, and complicated error handling. The Java Concurrency Utilities (java.util.concurrent), introduced in Java 5 and modernised in Java 21, provide high-level abstractions to execute asynchronous tasks via managed thread pools.
1. The Executor Framework Hierarchy
The executor framework is organized as a tier of core interfaces:
+-------------------+
| Executor | (void execute(Runnable))
+-------------------+
^
|
+-------------------+
| ExecutorService | (submit, invokeAll, shutdown)
+-------------------+
^
|
+---------------------------+
| ScheduledExecutorService | (schedule, scheduleAtFixedRate)
+---------------------------+
Factory Methods in java.util.concurrent.Executors
| Factory Method | Underlying Thread Pool Configuration | Best Suited For |
|---|---|---|
newSingleThreadExecutor() | 1 reusable worker thread with an unbounded LinkedBlockingQueue | Strictly sequential task processing |
newFixedThreadPool(int nThreads) | Fixed pool of $N$ threads with an unbounded LinkedBlockingQueue | Resource-constrained, predictable steady workloads |
newCachedThreadPool() | 0 core threads, creates new threads on demand (60s idle keep-alive), SynchronousQueue | Bursty, short-lived asynchronous tasks |
newScheduledThreadPool(int corePoolSize) | Delayed work queue for recurring/scheduled tasks | Periodic background jobs, timer tasks |
newVirtualThreadPerTaskExecutor() (Java 21) | Spawns an ephemeral virtual thread for every submitted task | Massive-scale, I/O-bound concurrency |
2. Callable<V> vs. Runnable
The Runnable interface has been part of Java since 1.0, while Callable<V> was introduced in Java 5 to support return values and checked exceptions.
| Feature | java.lang.Runnable | java.util.concurrent.Callable<V> |
|---|---|---|
| Method Signature | void run() | V call() throws Exception |
| Return Value | Returns void | Returns value of generic type V |
| Checked Exceptions | Cannot throw checked exceptions | Can throw any checked Exception |
| Executor Submission | executor.execute(r) or executor.submit(r) | executor.submit(c) |
// Runnable lambda: No return value, must catch checked exceptions internally
Runnable runnableTask = () -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Runnable completed");
};
// Callable lambda: Returns Integer, throws checked Exception directly!
Callable<Integer> callableTask = () -> {
Thread.sleep(500); // Throws InterruptedException directly!
return 42;
};
3. Working with Future<V>
When a task is submitted to an ExecutorService via submit(), the framework immediately returns a Future<V> representing the pending asynchronous result.
Key Methods of Future<V>:
V get(): Blocks synchronously until computation completes.- Throws
InterruptedException: If the calling thread was interrupted while waiting. - Throws
ExecutionException: If the background task threw an exception (checked or unchecked). The underlying root cause exception is retrieved viae.getCause(). - Throws
CancellationException: If the task was cancelled viacancel().
- Throws
V get(long timeout, TimeUnit unit): Blocks up to the specified timeout. ThrowsTimeoutExceptionif the computation does not finish within the allotted time.boolean isDone(): Returnstrueif the task completed normally, was cancelled, or aborted with an exception.boolean isCancelled(): Returnstrueif the task was cancelled before normal completion.boolean cancel(boolean mayInterruptIfRunning): Attempts to cancel task execution. IfmayInterruptIfRunningistrue, the worker thread executing the task is interrupted viaThread.interrupt().
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
if (true) throw new IOException("Disk failure during read");
return "Data Payload";
});
try {
String result = future.get(); // Blocks until completion
System.out.println("Result: " + result);
} catch (InterruptedException e) {
System.out.println("Caller thread was interrupted!");
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // Retrieves java.io.IOException
System.out.println("Task failed with root cause: " + cause.getMessage());
} finally {
executor.shutdown();
}
4. Bulk Task Execution: invokeAll() vs. invokeAny()
ExecutorService provides convenient bulk execution methods:
A. invokeAll()
List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks): Executes all tasks, blocking until all tasks complete (normally or exceptionally).- Returns a
List<Future<T>>in the exact same iteration order as the input collection.
B. invokeAny()
T invokeAny(Collection<? extends Callable<T>> tasks): Executes tasks and blocks until ONE task completes successfully (without throwing an exception).- Returns the result of that fastest successful task and automatically cancels all remaining uncompleted tasks.
List<Callable<String>> tasks = List.of(
() -> { Thread.sleep(300); return "Server-A Response"; },
() -> { Thread.sleep(100); return "Server-B Response"; }, // Fastest!
() -> { Thread.sleep(500); return "Server-C Response"; }
);
ExecutorService exec = Executors.newFixedThreadPool(3);
String fastestResult = exec.invokeAny(tasks);
System.out.println("Fastest result: " + fastestResult); // "Server-B Response"
exec.shutdown();
5. ScheduledExecutorService Timers and Periodic Tasks
ScheduledExecutorService enables delayed execution and recurring periodic tasks:
Scheduling Methods & Timing Semantics:
schedule(Callable<V> callable, long delay, TimeUnit unit): One-shot execution afterdelay.scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):- Initiates execution at fixed intervals: $T_0, T_0 + \text{period}, T_0 + 2\times\text{period}, \dots$
- If an execution takes longer than
period, subsequent executions are delayed to prevent concurrent runs of the same task, but the scheduler catches up afterwards.
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit):- Enforces a fixed delay between the completion of one execution and the initiation of the next.
- Next Start Time = (End Time of Previous Execution) +
delay.
scheduleAtFixedRate (period = 5s, task duration = 2s):
|-- 2s task --| 3s idle |-- 2s task --| 3s idle |
0s 2s 5s 7s 10s
scheduleWithFixedDelay (delay = 5s, task duration = 2s):
|-- 2s task --|------ 5s delay ------|-- 2s task --|------ 5s delay ------|
0s 2s 7s 9s 14s
[!WARNING] If any periodic execution throws an unhandled runtime exception or error, all subsequent executions of the scheduled task are permanently suppressed and terminated without notifying the scheduler or crashing the pool!
6. Asynchronous Pipelines with CompletableFuture
Java 8 introduced CompletableFuture<T> (implementing Future<T> and CompletionStage<T>) to build non-blocking asynchronous processing pipelines:
CompletableFuture.supplyAsync(() -> fetchUserData(userId), executor)
.thenApply(user -> user.getEmail()) // Transform result
.thenAccept(email -> sendNotification(email)) // Consume result
.exceptionally(ex -> {
System.err.println("Pipeline failed: " + ex.getMessage());
return null;
});
supplyAsync(Supplier<U>): Initiates asynchronous task returning a value.runAsync(Runnable): Initiates asynchronous task returningVoid.thenApply(Function): Maps/transforms the completed value.thenAccept(Consumer): Consumes the completed value without returning a result.thenCombine(otherFuture, BiFunction): Combines results of two independent futures.join()vs.get():join()does not throw checked exceptions (it throws uncheckedCompletionException), making it ideal for lambda pipelines.
7. Lifecycle, Graceful Shutdown, and AutoCloseable
An ExecutorService maintains active non-daemon OS worker threads that will keep the JVM process alive indefinitely if not properly shut down.
The Canonical Graceful Shutdown Protocol:
ExecutorService executor = Executors.newFixedThreadPool(4);
// Submit tasks...
executor.shutdown(); // 1. Stop accepting new tasks; allow active/queued tasks to finish
try {
// 2. Wait up to 5 seconds for existing tasks to complete
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
List<Runnable> droppedTasks = executor.shutdownNow(); // 3. Cancel active tasks via interrupt
System.out.println("Dropped tasks count: " + droppedTasks.size());
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
shutdown(): Rejects new tasks (RejectedExecutionException), but completes all previously submitted and queued tasks.shutdownNow(): Attempts to cancel active tasks viaThread.interrupt(), drains the queue, and returnsList<Runnable>of unstarted tasks.isShutdown(): Returnstrueonceshutdown()orshutdownNow()has been called.isTerminated(): Returnstrueonly after all tasks have finished following shutdown.
Java 19+ / Java 21 AutoCloseable Integration
In modern Java, ExecutorService extends AutoCloseable. Using ExecutorService inside a try-with-resources statement automatically invokes close(), which calls shutdown() and blocks in awaitTermination() indefinitely until all tasks complete:
try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {
exec.submit(() -> System.out.println("Task 1 processing"));
exec.submit(() -> System.out.println("Task 2 processing"));
} // Auto-closes: invokes shutdown() and waits for all tasks to terminate!
// Reaching this line guarantees that all submitted tasks are fully finished!
A background task submitted to an ExecutorService throws an unhandled NullPointerException during execution. What happens when the main thread calls future.get() on the returned Future?
A ScheduledExecutorService is configured with scheduleWithFixedDelay(task, 0, 5, TimeUnit.SECONDS). If the task takes 3 seconds to complete each run, what is the exact interval between the START of run 1 and the START of run 2?
Which statement accurately describes the behavior of executor.invokeAny(taskList) when supplied with a collection of three Callable tasks?
Consider the following code snippet running in Java SE 21:
What is guaranteed regarding the output?try (ExecutorService exec = Executors.newFixedThreadPool(2)) {
exec.submit(() -> {
try { Thread.sleep(500); } catch (Exception e) {}
System.out.print("A ");
});
exec.submit(() -> {
try { Thread.sleep(500); } catch (Exception e) {}
System.out.print("B ");
});
}
System.out.print("C ");