Concurrency

Executors & Thread Pools

Executors separate task submission from execution policy. Pools reuse threads, bound resource usage, and return Futures. Size them for the workload, bound their queues, and always shut them down.
  • Prefer executors and tasks to raw threads (EJ 80): policy lives in one place
  • newFixedThreadPool(n) for CPU work; newVirtualThreadPerTaskExecutor() for blocking I/O work
  • CPU-bound sizing: ~Runtime.getRuntime().availableProcessors(); I/O-bound: don't pool — virtual threads
  • Future.get blocks — prefer Completable Future for composition
  • Unbounded queues (newFixedThreadPool's default!) hide overload until OOM — bound and reject instead
  • Shutdown ritual: shutdown()awaitTerminationshutdownNow() (or use close()/try-with-resources, Java 19+)
Submit, collect, shut down
try (ExecutorService pool = Executors.newFixedThreadPool(
        Runtime.getRuntime().availableProcessors())) {      // AutoCloseable since 19

    List<Future<Report>> futures = tasks.stream()
            .map(t -> pool.submit(() -> analyze(t)))         // Callable<Report>
            .toList();

    for (Future<Report> f : futures)
        merge(f.get());                                       // rethrows task failure as ExecutionException
}

JCiP's framing: a task is a unit of work; an executor is a policy about threads. Changing "200 requests each on its own thread" to "a pool of 16 with a bounded queue and caller-runs rejection" is a one-line change when submission and execution are decoupled — and impossible when new Thread calls are scattered through the code (EJ 80).

A production-grade pool is explicit about its policy
ExecutorService pool = new ThreadPoolExecutor(
        8, 8,                                  // core = max: fixed size
        0, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(1_000),       // BOUNDED queue
        Thread.ofPlatform().name("worker-", 0).factory(),
        new ThreadPoolExecutor.CallerRunsPolicy());   // backpressure, not OOM

Sizing (JCiP ch. 8): CPU-bound → N_cpu (a queue of waiting tasks costs nothing; extra threads cost context switches). Mixed → N_cpu × (1 + wait/compute). I/O-dominated → the formula explodes toward thousands, which is the signal that pooling platform threads is the wrong model: use Virtual Threads and stop sizing. ForkJoinPool serves divide-and-conquer and Parallel Streams via work-stealing deques; it is not a general blocking-task pool.

Sources
  • Java Concurrency in PracticeCh. 6, 8 — Task Execution; Applying Thread Pools
  • Effective Java (3rd ed.)Item 80
  • Core Java, Volume I: Fundamentals (13th ed.)Ch. 10.4 — Coordinating Tasks