What is an ExecutorService, and what thread pool types does Executors provide?
Quick Answer
ExecutorService decouples task submission from thread management, letting you submit Runnable/Callable tasks to a managed pool instead of creating raw Threads yourself. The Executors factory provides fixedThreadPool (bounded pool size), cachedThreadPool (grows/shrinks, reuses idle threads), singleThreadExecutor (one worker, serial execution), and scheduledThreadPool (delayed/periodic tasks) — though modern guidance favors constructing ThreadPoolExecutor directly for production tuning control.
Detailed Answer
ExecutorService is the standard abstraction for running asynchronous tasks: you submit work (Runnable/Callable), and a managed pool of worker threads executes it, handling thread creation, reuse, and shutdown for you.
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> future = pool.submit(() -> compute());
pool.execute(() -> doSomething()); // fire-and-forget
pool.shutdown(); // stop accepting new tasks, let queued/running ones finish
Executors provides several convenience factories (all really just pre-configured ThreadPoolExecutors):
newFixedThreadPool(n): a fixed number of threads; extra tasks queue up on an unbounded queue until a thread frees up. Good for CPU-bound work with a known, stable concurrency level.newCachedThreadPool(): creates new threads as needed, reuses idle ones, and kills threads idle for 60s. Good for many short-lived, bursty tasks, but can create unbounded threads under sustained load if tasks queue up faster than they finish.newSingleThreadExecutor(): exactly one worker thread, tasks run serially in submission order — useful when you need strict ordering.newScheduledThreadPool(n): supports delayed and periodic task execution (schedule,scheduleAtFixedRate,scheduleWithFixedDelay).
Modern guidance: many teams now avoid the Executors factory methods in production, because newFixedThreadPool/newCachedThreadPool use unbounded queues or unbounded thread creation respectively — both are potential resource-exhaustion risks under load. Constructing new ThreadPoolExecutor(...) directly lets you specify a bounded work queue and an explicit rejection policy, giving predictable backpressure instead of silently accumulating memory.
Always call shutdown() (graceful) or shutdownNow() (attempts to cancel in-flight tasks) when done, and typically awaitTermination(...) to wait for a clean stop — otherwise the JVM may hang on exit waiting for non-daemon pool threads.