Executors & Thread Pools
The Executor framework decouples task submission from thread management, and thread pools reuse a bounded set of worker threads to run many tasks efficiently instead of creating a new Thread for every unit of work.
Creating a new Thread for every task is expensive and unbounded — thread creation has real overhead, and nothing stops an application from spawning thousands of threads under load, exhausting memory and CPU scheduling capacity. The java.util.concurrent Executor framework solves this by separating what to run (a Runnable or Callable, from the earlier topic) from how and when to run it, delegated to an Executor implementation — most commonly a thread pool that reuses a bounded, managed set of worker threads across many submitted tasks.
The Executor Interface: Decoupling Submission From Execution
java.util.concurrent.Executor is a minimal interface with a single method: void execute(Runnable command). Code that wants work done depends only on this interface, not on how or where the work actually runs — that decision belongs entirely to whichever Executor implementation is plugged in, which could run the task on a new thread, a pooled thread, the calling thread, or not at all yet (e.g. queued for later).
This tiny interface is the foundation everything else in this topic builds on — ExecutorService adds much more, but every Executor honors this same core contract of 'submit and forget.'
public interface Executor {
void execute(Runnable command);
}ExecutorService: A Richer Task-Management API
ExecutorService extends Executor with lifecycle management (shutdown(), shutdownNow(), isShutdown(), isTerminated(), awaitTermination()) and richer task submission that returns a handle to track completion and results: submit(Runnable), submit(Callable<V>) — both returning a Future (covered in the next topic) — plus batch operations like invokeAll() and invokeAny().
ExecutorService is what almost all real code uses in practice — plain Executor is mostly a conceptual base type.
import java.util.concurrent.*;
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.execute(() -> System.out.println("fire and forget"));
Future<Integer> result = pool.submit(() -> 21 * 2);
pool.shutdown();Thread Pools: Why Reuse Threads
A thread pool maintains a set of pre-created (or lazily created, up to a bound) worker threads that repeatedly pull tasks from an internal queue and run them, instead of a new OS thread being spun up and torn down for every single task. This amortizes thread creation/teardown cost across many tasks and lets an application cap the maximum number of concurrently running threads, protecting it from unbounded resource exhaustion under heavy load.
The key idea: worker threads are long-lived and reused across many tasks, rather than one thread per task.
Task submitted --> internal work queue --> idle worker thread picks it up --> runs task --> worker returns to pool, ready for next taskExecutors Factory Methods: Fixed Thread Pool
Executors.newFixedThreadPool(int n) creates a pool with exactly n threads that stay alive for the pool's lifetime, backed by an unbounded work queue. If all n threads are busy, additional submitted tasks simply wait in the queue rather than spawning extra threads. This is a good default when you want a predictable, capped level of concurrency.
With only 4 threads, at most 4 of these 10 tasks run truly concurrently at any instant; the rest wait in the internal queue until a worker thread becomes free.
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
int taskId = i;
pool.execute(() -> System.out.println("task " + taskId + " on " + Thread.currentThread().getName()));
}
pool.shutdown();Executors Factory Methods: Cached Thread Pool
Executors.newCachedThreadPool() creates threads on demand as needed (with no fixed upper bound in practice), reuses previously created idle threads when available, and terminates threads that have sat idle for 60 seconds. This suits workloads with many short-lived, bursty tasks, but its lack of a bound makes it risky for workloads that could submit an unexpectedly large number of long-running tasks — it can create an unbounded number of threads under sustained load.
Cached pools are convenient for I/O-bound bursts, but the absence of a thread cap is exactly why many teams prefer manually configuring a ThreadPoolExecutor with an explicit bound in production systems.
ExecutorService pool = Executors.newCachedThreadPool();
// good for many short bursts of independent work; risky if tasks can pile up and blockExecutors Factory Methods: Scheduled Executor
Executors.newScheduledThreadPool(int n) returns a ScheduledExecutorService, which adds the ability to run tasks after a delay or on a recurring schedule: schedule() (one-shot, after a delay), scheduleAtFixedRate() (repeats at a fixed rate from the start of each execution), and scheduleWithFixedDelay() (repeats with a fixed delay measured from the end of the previous execution).
scheduleAtFixedRate's period is measured between successive start times (roughly), while scheduleWithFixedDelay waits the given delay after each execution finishes before starting the next — the two behave differently if a task occasionally runs longer than the period.
import java.util.concurrent.*;
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.schedule(() -> System.out.println("runs once after 2s"), 2, TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(() -> System.out.println("tick"), 0, 1, TimeUnit.SECONDS);
// remember to shutdown() the scheduler when the recurring task is no longer neededExecutors Factory Methods: Single-Thread Executor
Executors.newSingleThreadExecutor() creates a pool of exactly one worker thread with an unbounded queue, guaranteeing tasks execute sequentially, in submission order, never concurrently with each other. This is useful when tasks must not run in parallel (e.g. sequential writes to a single resource) but you still want them to run off the calling thread.
Task Submission Methods Compared
submit()-based methods are almost always preferable to execute() in application code, since they let you observe both success and failure through the returned Future rather than losing exceptions to a default handler.
execute(Runnable) fire-and-forget, no return value, exceptions go to uncaught handler
submit(Runnable) returns Future<?>, exceptions captured, surfaced via get()
submit(Callable<V>) returns Future<V>, result and exceptions surfaced via get()
invokeAll(Collection<Callable>) runs all, blocks until all complete, returns List<Future<V>>
invokeAny(Collection<Callable>) runs all, returns the result of whichever finishes first successfullyShutdown and Lifecycle
An ExecutorService does not shut itself down automatically — its worker threads are typically non-daemon by default in the standard factory methods, meaning a program that never shuts down its executor may never exit. shutdown() initiates an orderly shutdown: no new tasks are accepted, but previously submitted tasks are allowed to finish. shutdownNow() attempts to stop all actively executing tasks (via interruption) and returns the list of tasks that were still waiting in the queue, never started.
This is the standard, recommended shutdown idiom: attempt a graceful shutdown, wait a bounded amount of time, and forcefully shut down if the pool hasn't drained in that window.
Since Java 19, ExecutorService also implements AutoCloseable, so it can be used in a try-with-resources block, whose close() method calls shutdown() (and waits, escalating to shutdownNow() if interrupted) automatically.
ExecutorService pool = Executors.newFixedThreadPool(4);
// ... submit tasks ...
pool.shutdown(); // stop accepting new tasks, let existing ones finish
try {
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
pool.shutdownNow(); // force-stop if it didn't finish in time
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt(); // restore interrupt status
}Practical Usage: Choosing a Pool Type
Fixed pool: predictable, CPU-bound or resource-bound workloads where you want a hard cap on concurrency (often sized near the number of CPU cores for CPU-bound work). Cached pool: many short, bursty, I/O-bound tasks where unbounded thread growth is acceptable and expected to self-limit. Scheduled pool: periodic or delayed tasks (health checks, cache eviction, polling). Single-thread executor: tasks that must run strictly sequentially off the calling thread. For fine control beyond the Executors factory presets, ThreadPoolExecutor can be constructed directly with an explicit core/max pool size, queue type, and rejection policy.
Common Pitfall: Forgetting to Shut Down
Because standard executor thread pools use non-daemon threads by default, an application (or a test, or a short-lived tool) that creates an ExecutorService and never calls shutdown() can hang indefinitely at exit, since the JVM waits for all non-daemon threads to finish before terminating.
Key Takeaways
Executor decouples task submission (execute(Runnable)) from the decision of how/where tasks actually run.
ExecutorService adds lifecycle management and richer submission (submit, invokeAll, invokeAny) returning Futures.
Thread pools reuse a bounded (or self-limiting) set of worker threads across many tasks, avoiding the cost and risk of one-thread-per-task.
Executors provides factory presets: newFixedThreadPool, newCachedThreadPool, newScheduledThreadPool, newSingleThreadExecutor — each suited to a different workload shape.
shutdown() stops accepting new tasks and lets existing ones finish; shutdownNow() attempts to interrupt and stop running tasks immediately.
An ExecutorService must be explicitly shut down (or used in try-with-resources since Java 19) or its non-daemon threads can keep the JVM alive indefinitely.
ExecutorService pool = Executors.newFixedThreadPool(4); // ... submit tasks, use results ... // program never calls pool.shutdown() -- JVM hangs at exit
ExecutorService pool = Executors.newFixedThreadPool(4); try { // ... submit tasks ... } finally { pool.shutdown(); }
Standard thread pool threads are non-daemon by default, so a never-shutdown ExecutorService can prevent the JVM from ever exiting; always shut down explicitly (finally block, or try-with-resources on Java 19+).
Executors.newCachedThreadPool(); // used for a workload of thousands of long-running, blocking tasks new ThreadPoolExecutor(coreSize, maxSize, keepAlive, unit, boundedQueue, rejectionPolicy); // explicit bounds sized for the actual workload
A cached pool has no upper bound on thread creation; feeding it a large volume of long-running tasks can create an unbounded number of threads and exhaust system resources. Use a bounded pool sized for the workload instead.
pool.execute(() -> { throw new RuntimeException("fail"); }); // exception silently lost to default handler
Future<?> f = pool.submit(() -> { throw new RuntimeException("fail"); }); try { f.get(); } catch (ExecutionException e) { /* handle e.getCause() */ } execute() gives no way to observe an exception thrown by the task (it goes to the thread's uncaught exception handler, easy to miss); submit() captures it in the returned Future, surfaced when get() is called.
What single method does the Executor interface declare, and what is its purpose?
void execute(Runnable command). Its purpose is to decouple submitting a task from deciding how and where that task actually runs — the calling code depends only on this interface, not on the specific execution strategy.
What does ExecutorService add on top of the base Executor interface?
Lifecycle management (shutdown(), shutdownNow(), isShutdown(), isTerminated(), awaitTermination()) and richer task submission methods — submit(Runnable)/submit(Callable<V>), which return a Future, plus invokeAll() and invokeAny() for batches of tasks.
Why is a thread pool generally preferable to creating a new Thread for every task?
Creating and tearing down OS threads has real overhead, and doing it per-task with no bound risks unbounded thread growth under load, exhausting memory and CPU scheduling capacity. A thread pool reuses a bounded (or self-limiting) set of long-lived worker threads across many tasks, amortizing that cost and capping concurrency.
What's the key behavioral difference between Executors.newFixedThreadPool(n) and Executors.newCachedThreadPool()?
A fixed pool always has exactly n worker threads for its lifetime and queues excess tasks when all are busy, providing a hard concurrency cap. A cached pool creates threads on demand with no fixed upper bound, reuses idle ones, and terminates threads idle for 60 seconds — good for short bursty tasks but risky for sustained high-volume or long-running workloads since it can grow unboundedly.
What is the difference between shutdown() and shutdownNow() on ExecutorService?
shutdown() performs an orderly shutdown: no new tasks are accepted, but tasks already submitted (running or queued) are allowed to complete. shutdownNow() attempts to stop all actively executing tasks immediately (typically via thread interruption) and returns the list of tasks that were still queued and never started.
Why does execute(Runnable) make it easy to silently lose an exception thrown by a task, and how does submit() avoid this?
A Runnable passed to execute() that throws an unchecked exception has that exception routed to the running thread's uncaught exception handler (by default, printed to stderr), which the code that called execute() has no direct way to observe or react to. submit() instead captures any exception the task throws and stores it in the returned Future, which is surfaced (wrapped in ExecutionException) when get() is called.
What does ScheduledExecutorService add over a plain ExecutorService, and what's the difference between scheduleAtFixedRate and scheduleWithFixedDelay?
It adds the ability to run tasks after a delay or repeatedly on a schedule, via schedule(), scheduleAtFixedRate(), and scheduleWithFixedDelay(). scheduleAtFixedRate() aims to start successive executions at a fixed period measured from each execution's start; scheduleWithFixedDelay() waits the given delay after each execution finishes before starting the next, so a long-running execution shifts all subsequent start times later.
Write code that submits 5 Callable<Integer> tasks (each returning its index squared) to a fixed thread pool, collects all results, and shuts the pool down properly.
import java.util.*; import java.util.concurrent.*; public class SquareTasks { public static void main(String[] args) throws InterruptedException, ExecutionException { ExecutorService pool = Executors.newFixedThreadPool(3); List<Callable<Integer>> tasks = new ArrayList<>(); for (int i = 0; i < 5; i++) { int n = i; tasks.add(() -> n * n); } List<Future<Integer>> futures = pool.invokeAll(tasks); for (Future<Integer> f : futures) { System.out.println(f.get()); } pool.shutdown(); } }
Prints 0, 1, 4, 9, 16 (in submission order, since invokeAll returns futures in the same order as the input list, even though the underlying tasks may have completed out of order across the 3 pool threads). invokeAll() blocks until every task has completed before returning the list of Futures.
In what order do A, B, and C print, and why is that order guaranteed here?
They print in order: A, then B, then C. A single-thread executor has exactly one worker thread processing its internal task queue strictly in FIFO submission order, so tasks submitted to it never run concurrently with each other and always execute in the order they were submitted.
Write a ScheduledExecutorService example that prints a heartbeat message every 2 seconds, and shuts the scheduler down after 10 seconds using a separate one-shot schedule.
import java.util.concurrent.*; public class Heartbeat { public static void main(String[] args) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); ScheduledFuture<?> heartbeat = scheduler.scheduleAtFixedRate( () -> System.out.println("heartbeat at " + System.currentTimeMillis()), 0, 2, TimeUnit.SECONDS); scheduler.schedule(() -> { heartbeat.cancel(false); // stop the recurring task scheduler.shutdown(); }, 10, TimeUnit.SECONDS); } }
The scheduler prints a heartbeat immediately and then every 2 seconds (at roughly 0, 2, 4, 6, 8, 10s). The one-shot task scheduled for 10 seconds cancels the recurring heartbeat task and shuts the scheduler down, so no further heartbeats print and the pool's threads stop, allowing the JVM to exit.
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.execute(() -> System.out.println("A"));
pool.execute(() -> System.out.println("B"));
pool.execute(() -> System.out.println("C"));
pool.shutdown();