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.
Last updated: September 2026

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 MethodUnderlying Thread Pool ConfigurationBest Suited For
newSingleThreadExecutor()1 reusable worker thread with an unbounded LinkedBlockingQueueStrictly sequential task processing
newFixedThreadPool(int nThreads)Fixed pool of $N$ threads with an unbounded LinkedBlockingQueueResource-constrained, predictable steady workloads
newCachedThreadPool()0 core threads, creates new threads on demand (60s idle keep-alive), SynchronousQueueBursty, short-lived asynchronous tasks
newScheduledThreadPool(int corePoolSize)Delayed work queue for recurring/scheduled tasksPeriodic background jobs, timer tasks
newVirtualThreadPerTaskExecutor() (Java 21)Spawns an ephemeral virtual thread for every submitted taskMassive-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.

Featurejava.lang.Runnablejava.util.concurrent.Callable<V>
Method Signaturevoid run()V call() throws Exception
Return ValueReturns voidReturns value of generic type V
Checked ExceptionsCannot throw checked exceptionsCan throw any checked Exception
Executor Submissionexecutor.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 via e.getCause().
    • Throws CancellationException: If the task was cancelled via cancel().
  • V get(long timeout, TimeUnit unit): Blocks up to the specified timeout. Throws TimeoutException if the computation does not finish within the allotted time.
  • boolean isDone(): Returns true if the task completed normally, was cancelled, or aborted with an exception.
  • boolean isCancelled(): Returns true if the task was cancelled before normal completion.
  • boolean cancel(boolean mayInterruptIfRunning): Attempts to cancel task execution. If mayInterruptIfRunning is true, the worker thread executing the task is interrupted via Thread.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:

  1. schedule(Callable<V> callable, long delay, TimeUnit unit): One-shot execution after delay.
  2. 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.
  3. 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 returning Void.
  • 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 unchecked CompletionException), 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 via Thread.interrupt(), drains the queue, and returns List<Runnable> of unstarted tasks.
  • isShutdown(): Returns true once shutdown() or shutdownNow() has been called.
  • isTerminated(): Returns true only 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!
Loading diagram...
ThreadPoolExecutor Architecture and Task Processing Flow
Test Your Knowledge

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
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

Which statement accurately describes the behavior of executor.invokeAny(taskList) when supplied with a collection of three Callable tasks?

A
B
C
D
Test Your Knowledge

Consider the following code snippet running in Java SE 21:

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 ");
What is guaranteed regarding the output?

A
B
C
D