Q23 of 40 · Core Java

How does ExecutorService work? Why use it over creating threads directly?

Core JavaMidexecutorservicethread-poolconcurrencycallablejava-threads

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 via get().
  • 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
    }
}

// WHAT INTERVIEWERS LOOK FOR

Thread pool reuse vs per-task thread creation, the four factory methods and their trade-offs, execute vs submit vs invokeAll, and the shutdown/awaitTermination lifecycle pattern. Strong answers mention the try-finally shutdown idiom.

// COMMON PITFALL

Forgetting to shut down the ExecutorService. An un-shutdown pool's non-daemon threads keep the JVM alive after main() returns, causing the process to hang. In tests, this appears as a test suite that never finishes.