What does ExecutorService do?
ExecutorService is the primary interface in Java for managing thread pools and executing asynchronous tasks.
Junior Level
Basic Understanding
ExecutorService is the primary interface in Java for managing thread pools and executing asynchronous tasks.
Without ExecutorService: you manually create threads (new Thread().start()), control their lifecycle, and handle errors. With ExecutorService: you only submit tasks and get results — the framework handles thread management for you. It separates “what to do” (the task) from “how and when to do it” (the execution mechanism).
Simple Example
// Creating ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submitting a task
executor.submit(() -> {
System.out.println("Task executed!");
});
// Shutdown
executor.shutdown();
Main Ways to Submit Tasks
| Method | Returns | Exceptions | When to Use |
|---|---|---|---|
execute(Runnable) |
void | Printed to console | “Fire and forget” |
submit(Runnable) |
Future<?> | Wrapped in ExecutionException — wrapper is needed to distinguish task errors from ExecutorService errors. Original exception available via e.getCause(). |
Need to track completion |
submit(Callable<T>) |
Future |
Wrapped in ExecutionException — wrapper is needed to distinguish task errors from ExecutorService errors. Original exception available via e.getCause(). |
Need a result |
execute() vs submit()
// execute() — "fire and forget"
executor.execute(() -> {
System.out.println("Running...");
// Error here — just prints to System.err
});
// submit() — can get result and error
Future<?> future = executor.submit(() -> {
System.out.println("Running...");
// Error here — will be in future.get()
});
// Checking completion
if (future.isDone()) {
System.out.println("Task completed");
}
Shutting Down the Pool
// Method 1: Graceful shutdown
executor.shutdown(); // Stops accepting new tasks
executor.awaitTermination(60, TimeUnit.SECONDS); // Wait up to 60 seconds
// Method 2: Forceful shutdown
executor.shutdownNow(); // Interrupts active tasks
Middle Level
ExecutorService Lifecycle
┌──────────┐ shutdown() ┌───────────┐ all tasks ┌─────────┐
│ RUNNING │ ──────────────→ │ SHUTDOWN │ ────────────→ │ TIDYING │
│ │ │ │ │ │
│ accepts │ shutdownNow() │ does not │ │ │
│ tasks │ ──────────────→ │ accept │ │ │
│ │ │ tasks │ │ │
│ processes│ ←───────────── │ finishes │ │ │
│ │ interrupted │ queue │ │ │
└──────────┘ └───────────┘ └────┬────┘
│ │
│ │ terminated()
│ ▼
│ ┌───────────┐
└────────────────────────────────────────────────│ TERMINATED│
└───────────┘
| State | Description |
|---|---|
| RUNNING | Accepts and processes tasks |
| SHUTDOWN | Does not accept new, finishes queue |
| STOP | Does not accept, interrupts active |
| TIDYING | All tasks finished, threads stopped |
| TERMINATED | terminated() executed |
submit(): Future and Error Handling
// Callable — returns a result
Future<String> future = executor.submit(() -> {
return "Result";
});
// Getting the result (blocks!)
String result = future.get();
// Getting with timeout
try {
String result = future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true); // Cancel the task
}
// cancel(true) sends interrupt() to the task.
// The task must check Thread.interrupted() itself and terminate.
// If the task doesn't handle interrupts — it will continue running!
invokeAll() and invokeAny()
// invokeAll() — waits for ALL tasks
List<Callable<String>> tasks = List.of(
() -> "Task 1",
() -> "Task 2",
() -> "Task 3"
);
List<Future<String>> futures = executor.invokeAll(tasks);
// Blocks until all complete
// invokeAny() — waits for the FIRST successful task
String result = executor.invokeAny(tasks);
// Returns the result of the first completed task, cancels the rest
Proper Pool Shutdown
public void shutdownGracefully(ExecutorService executor) {
executor.shutdown(); // 1. Stop accepting
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { // 2. Wait
executor.shutdownNow(); // 3. Force
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
System.err.println("Pool did not terminate");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt(); // Restore interrupt flag
}
}
submit() vs execute() — The Exception Trap
// execute() — exception thrown immediately
executor.execute(() -> {
throw new RuntimeException("Error!"); // → UncaughtExceptionHandler
});
// submit() — exception "hidden" in Future
Future<?> future = executor.submit(() -> {
throw new RuntimeException("Error!");
});
// No error until you call future.get()!
try {
future.get(); // → ExecutionException
} catch (ExecutionException e) {
System.out.println(e.getCause()); // → RuntimeException: Error!
}
Senior Level
Implementation details of ThreadPoolExecutor (ctl, queues, ThreadFactory) — in file [[12. What is a Thread Pool]].
Thread Pool Leaks
// BAD: creating pool inside a method
public void process() {
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> doWork());
executor.shutdown();
// If doWork() throws an exception — shutdown() is not called
// Threads leak!
}
// GOOD: pool is a singleton or managed by a container
@Component
public class TaskProcessor {
private final ExecutorService executor;
public TaskProcessor() {
this.executor = new ThreadPoolExecutor(...);
}
@PreDestroy
public void shutdown() {
shutdownGracefully(executor);
}
}
Correlation ID / Trace Context
// Problem: Trace ID is not propagated automatically
executor.submit(() -> {
// MDC.get("traceId") = null!
});
// Solution: wrapper
public class TracingExecutorService implements ExecutorService {
private final ExecutorService delegate;
public Future<?> submit(Runnable task) {
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
return delegate.submit(() -> {
if (mdcContext != null) MDC.setContextMap(mdcContext);
try {
task.run();
} finally {
MDC.clear();
}
});
}
}
Diagnostics
Metrics for Monitoring
// Output to Prometheus:
executor.getActiveCount(); // Active threads
executor.getQueue().size(); // Queue size
executor.getCompletedTaskCount(); // Completed tasks
executor.getTaskCount(); // Total tasks
executor.getPoolSize(); // Pool size
Future.isDone() / isCancelled()
Future<?> future = executor.submit(task);
// Without blocking
if (future.isDone()) {
System.out.println("Completed");
}
if (future.isCancelled()) {
System.out.println("Cancelled");
}
Best Practices
- Do not create ExecutorService inside a method — thread leak
- Use submit() — if you need error handling
- Always call shutdown() — in finally or @PreDestroy
- ArrayBlockingQueue — protection against OOM
- CallerRunsPolicy — back-pressure under overload
- Custom ThreadFactory — thread naming
- Trace ID propagation — for microservices
- Monitor queue.size() — growing queue = degradation
Interview Cheat Sheet
Must know:
- ExecutorService — interface for managing thread pools: task submission, result retrieval, graceful shutdown
- execute(Runnable) = void, exceptions print to console; submit() = Future, exceptions hidden in ExecutionException
- submit(Callable
) returns Future — can get result and handle error via get() - Lifecycle: RUNNING → SHUTDOWN (shutdown()) → STOP (shutdownNow()) → TIDYING → TERMINATED
- invokeAll() — waits for ALL tasks; invokeAny() — returns result of FIRST successful, cancels the rest
- Graceful shutdown: shutdown() → awaitTermination(timeout) → shutdownNow() → awaitTermination
- Future.get(timeout) throws TimeoutException — on timeout, call future.cancel(true)
- ExecutorService should be managed by a container (Spring @Bean) or singleton, NOT created inside a method
Frequent follow-up questions:
- What’s the difference between execute() vs submit()? — execute is “fire and forget”, submit allows tracking result and error via Future
- Why doesn’t submit() throw an exception immediately? — The exception is wrapped in ExecutionException and only available via future.get().getCause()
- How does shutdown() differ from shutdownNow()? — shutdown finishes the queue, shutdownNow interrupts active tasks via interrupt()
- How to propagate MDC/Trace ID in ExecutorService? — Wrapper: MDC.getCopyOfContextMap() before submit, MDC.setContextMap() inside the task
Red flags (DO NOT say):
- “I create ExecutorService inside a method” — thread leak if shutdown() is not called
- “submit() will show the exception immediately” — no, you need to call future.get()
- “shutdownNow() guaranteed stops all tasks” — no, it only sends interrupt(), tasks must handle it themselves
- “cancel(true) instantly kills the task” — no, it only sets the interrupted flag, the task must check it
Related topics:
- [[12. What is a Thread Pool]]
- [[13. What types of Thread Pool exist in Java]]
- [[16. What is the difference between Executors.newFixedThreadPool() and newCachedThreadPool()]]
- [[17. What is ForkJoinPool]]