Thread Pool Types
Thread Pool Types
Creating a raw Thread for every unit of work is expensive — thread creation involves kernel calls, stack allocation, and scheduling overhead. Thread pools solve this by keeping a set of pre-created threads alive and reusing them across many tasks. The java.util.concurrent.Executors factory class gives you four battle-tested pool flavours, each tuned for a different workload profile.
The Executors Factory — a Quick Map
All four factory methods return an ExecutorService (or ScheduledExecutorService), so you always interact with the same interface regardless of the pool type. Under the hood they differ in thread count, queue strategy, and how they handle peaks.
Fixed Thread Pool
Executors.newFixedThreadPool(n) creates exactly n worker threads and keeps them alive forever (until you shut the pool down). Submitted tasks queue in an unbounded LinkedBlockingQueue when all threads are busy.
With 20 tasks and a pool of 4, the first batch of 4 starts immediately; the remaining 16 wait in the queue and drain four at a time.
Runtime.getRuntime().availableProcessors() or a small multiple). A fixed pool prevents unbounded resource consumption and avoids the context-switching storm that comes from having far more runnable threads than cores.
ThreadPoolExecutor directly with a bounded ArrayBlockingQueue and a rejection policy.
Cached Thread Pool
Executors.newCachedThreadPool() creates new threads on demand and reuses idle ones. A thread that sits idle for 60 seconds is terminated and removed from the pool, so the pool shrinks back to zero when quiet.
Submitting 100 tasks may create up to 100 threads if they all arrive before the first ones finish. That is fine for short-lived I/O tasks — threads are cheap to keep around for 60 s and reused aggressively.
Single-Thread Executor
Executors.newSingleThreadExecutor() is a fixed pool of exactly one thread. Tasks execute sequentially in submission order, which makes it a handy serialisation primitive without any manual synchronisation.
newFixedThreadPool(1), the single-thread executor wraps its worker in a delegating wrapper. If the internal thread dies due to an uncaught exception, a new one is silently spawned to replace it, keeping the executor alive. A plain newFixedThreadPool(1) would not respawn.
Scheduled Thread Pool
Executors.newScheduledThreadPool(n) returns a ScheduledExecutorService that can run tasks after a delay or on a fixed schedule. It is the modern replacement for java.util.Timer.
There are two repeating variants: scheduleAtFixedRate aims for a constant wall-clock period (ideal for heartbeats, metrics polling), while scheduleWithFixedDelay waits for the previous run to finish before starting the delay counter (ideal when the task duration varies and overlap is dangerous).
Choosing the Right Pool
- CPU-bound, bounded concurrency —
newFixedThreadPool(cores) - Many short I/O tasks, variable rate —
newCachedThreadPool() - Sequential task queue, no synchronisation needed —
newSingleThreadExecutor() - Delayed or periodic execution —
newScheduledThreadPool(n)
Always Shut Down Properly
An ExecutorService that is never shut down keeps its threads alive, preventing JVM exit and leaking resources. Use the two-phase shutdown idiom:
System.exit() may mask leaked executors during development, but in a long-running service they accumulate over time and exhaust thread stack memory.
Summary
The Executors factory gives you four ready-made pool types. Fixed pools cap concurrency for CPU-bound work; cached pools elastically absorb I/O bursts; single-thread executors serialise work; and scheduled pools replace Timer for deferred or periodic tasks. Always shut executors down and, for production workloads, prefer building a ThreadPoolExecutor directly when you need a bounded queue or a custom rejection policy.