ExecutorService & Thread Pools
ExecutorService manages a pool of threads that execute submitted tasks (Runnable or Callable). Executors factory methods create common pool types: fixed, cached, single, scheduled.
Thread pool = a restaurant kitchen with a fixed number of chefs. Tasks are orders on the ticket rail. An unbounded queue is a ticket rail that stretches to infinity -- eventually the kitchen drowns in paper.
ThreadPoolExecutor parameters: corePoolSize (threads kept alive), maximumPoolSize (max threads), keepAliveTime, BlockingQueue (task queue). Fixed pool: queue is unbounded LinkedBlockingQueue — tasks queue up indefinitely under load. Cached pool: queue is SynchronousQueue (no buffering), creates threads up to Integer.MAX_VALUE — can OOM under burst load. Always prefer explicit ThreadPoolExecutor construction over Executors factories in production. Shutdown: shutdown() stops accepting new tasks and lets queued tasks finish; shutdownNow() interrupts running tasks.
Thread pool sizing is workload-dependent: CPU-bound tasks benefit from N+1 threads (N = available cores); I/O-bound tasks can use many more. The ForkJoinPool (used by parallelStream() and CompletableFuture.supplyAsync()) uses work-stealing — idle threads steal tasks from busy threads' deques, improving utilization for recursive divide-and-conquer. With Java 21 virtual threads, the carrier thread pool is a ForkJoinPool of platform threads; one virtual thread per task eliminates the need for manual pool sizing for I/O-bound workloads. Structured concurrency (JEP 480, Java 21 preview) brings task-scoped lifetime management to prevent thread and resource leaks.
ExecutorService decouples task submission from thread management. For production, construct ThreadPoolExecutor explicitly with a bounded queue (e.g., ArrayBlockingQueue) and a RejectedExecutionHandler to prevent unbounded task accumulation. Fixed-size pools with LinkedBlockingQueue are the most common source of silent OOM bugs — the queue grows without bound when consumers are slower than producers. Always call shutdown() in a finally block or via try-with-resources (Java 19+ AutoCloseable) to prevent thread leaks.
Executors.newFixedThreadPool(n) uses an unbounded queue — if producers outpace consumers, the queue grows without bound and causes OutOfMemoryError, not backpressure.