Java 30: ExecutorService & Thread Pools
easy⏱ 5 mincoursejava
The four factories
newFixedThreadPool(n) — n threads, unbounded queue. newCachedThreadPool() — grow as needed, 60s idle timeout. newSingleThreadExecutor() — serial task execution. newScheduledThreadPool(n) — timed / periodic tasks. Or build your own ThreadPoolExecutor for full control.
ExecutorService exec = Executors.newFixedThreadPool(4);
Future<Integer> f = exec.submit(() -> compute());
int result = f.get(); // blocks until done
exec.shutdown();
Queue & rejection policy
ThreadPoolExecutor(core, max, keepalive, queue, rejHandler) — the real deal. Tasks go into queue until core threads can take them; pool grows to max only when queue is full. If queue AND pool are full, rejHandler runs — defaults: AbortPolicy (throws), CallerRunsPolicy (runs on submitter), DiscardPolicy (silent drop — dangerous).
Shutdown vs shutdownNow
shutdown(): stop accepting new work, finish running + queued. shutdownNow(): stop accepting, interrupt running threads, return queued-but-unstarted tasks. Always pair with awaitTermination(timeout) — and log any tasks that didn't finish.
Build a bounded pool
BoundedPool(coreSize, maxQueue) with submit(task) that (1) runs on the next free 'thread' (up to core), (2) queues if all busy (up to maxQueue), (3) rejects if both full — log the rejection. Submit 20 tasks to a pool(core=3, queue=5). Track concurrent execution count.
Never submit to a single, shared pool
Mixing long-running and short tasks in one pool causes head-of-line blocking. Use bulkheads — separate pools per concern (DB, HTTP, CPU-compute). If one pool is saturated, the others keep serving traffic.
Quiz: unbounded queue danger
Why is newFixedThreadPool's unbounded queue risky? Under load, the queue grows without limit — you'll OOM. Always bound your queue. Size it based on memory + acceptable latency. If you can't bound, at least add a circuit breaker upstream.