Q23 of 40 · Core Java
How does ExecutorService work? Why use it over creating threads directly?
Short answer
Short answer: ExecutorService manages a pool of worker threads. You submit tasks (Runnable or Callable) and the pool dispatches them to available threads, queuing excess tasks. This avoids the overhead of creating a thread per task, controls the degree of parallelism, and provides lifecycle management (shutdown, awaitTermination).
Detail
Creating a new Thread for every unit of work is expensive — a Java platform thread requires ~512KB of stack by default, and the OS context-switch overhead adds up quickly. ExecutorService decouples task submission from task execution by maintaining a pool of reusable threads.
Key implementations (via factory methods in Executors):
newFixedThreadPool(n)— n threads, tasks queue when all busy. Predictable resource usage.newCachedThreadPool()— creates threads on demand, reuses idle ones, terminates threads idle for 60s. Good for short-lived tasks; dangerous for long-lived tasks (unbounded thread creation).newSingleThreadExecutor()— one thread, all tasks sequential. Useful for serialising access to non-thread-safe resources.newScheduledThreadPool(n)— run tasks with delays or on a recurring schedule.
Task submission:
execute(Runnable)— fire-and-forget, no result.submit(Callable<V>)→Future<V>— result available later viaget().invokeAll(List<Callable<V>>)— submit all, block until all complete, return list of Futures.invokeAny(List<Callable<V>>)— return the first successful result, cancel the rest.
Lifecycle: always shut down: shutdown() stops accepting new tasks; awaitTermination() waits for in-flight tasks; shutdownNow() interrupts in-flight tasks and returns queued tasks unrun.
For test automation: parallel API smoke tests using invokeAll, parallel data setup, or concurrent load simulation — all map naturally to ExecutorService patterns.
// EXAMPLE
ExecutorServiceExample.java
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
// Parallel smoke test — hit 4 endpoints concurrently
List<String> endpoints = List.of("/health", "/users", "/orders", "/inventory");
ExecutorService pool = Executors.newFixedThreadPool(4);
try {
List<Callable<Integer>> tasks = endpoints.stream()
.map(ep -> (Callable<Integer>) () -> httpClient.get(ep).statusCode())
.toList();
List<Future<Integer>> futures = pool.invokeAll(tasks, 10, TimeUnit.SECONDS);
for (int i = 0; i < futures.size(); i++) {
int status = futures.get(i).get(); // blocks (already done by invokeAll)
System.out.printf("%s → %d%n", endpoints.get(i), status);
}
} finally {
pool.shutdown();
if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
pool.shutdownNow(); // interrupt stragglers
}
}