Detailed explanation
Java’s Executors class provides factory methods for creating executor services, which separate task submission from thread management. Instead of creating a Thread for every operation, you submit Runnable or Callable tasks and let an executor manage their execution. The four commonly discussed choices are newFixedThreadPool, newCachedThreadPool, newSingleThreadExecutor, and newScheduledThreadPool. They differ in worker limits, queueing behaviour, and scheduling support. In interviews, explain these trade-offs rather than only listing method names: choosing the wrong executor can cause excessive queue growth, too many threads, or unintended task delays.
newFixedThreadPool(n) allows at most n worker threads to execute tasks concurrently and places additional tasks in an unbounded shared queue. Workers are normally created on demand, rather than all being started immediately. For example, Executors.newFixedThreadPool(5) allows up to five tasks to run simultaneously, but it does not limit how many tasks callers can submit. newSingleThreadExecutor uses one worker and an unbounded queue, so tasks execute sequentially without overlapping. If its worker terminates because of an execution failure, a replacement is created when needed. Unlike a fixed pool of size one, the returned single-thread executor cannot be reconfigured to use additional workers.
newCachedThreadPool reuses available idle workers and creates new workers when none can accept a task. Internally, it uses a SynchronousQueue, which hands tasks directly to workers instead of storing a backlog. Idle workers are removed after 60 seconds. This suits bursts of short-lived tasks, but slow tasks or heavy submissions can create excessive threads. newScheduledThreadPool(corePoolSize) returns a ScheduledExecutorService for delayed and periodic execution. scheduleAtFixedRate targets a regular cadence, whereas scheduleWithFixedDelay waits a specified interval after each execution finishes. Successive executions of the same periodic task do not overlap.
Other factories include newSingleThreadScheduledExecutor, newWorkStealingPool, and, since Java 21, newVirtualThreadPerTaskExecutor. Work-stealing executors use ForkJoinPool; the virtual-thread executor creates a new virtual thread per task rather than pooling threads. Always shut down executors when finished. For production workloads requiring bounded queues and explicit overload handling, construct a ThreadPoolExecutor directly instead of relying on these convenient defaults.
Key points to remember
- newFixedThreadPool(n): at most n active workers, with an unbounded queue for waiting tasks.
- newCachedThreadPool(): reuses idle workers, creates workers as needed, and removes workers idle for 60 seconds.
- newSingleThreadExecutor(): one worker executes queued tasks sequentially; its worker count cannot be reconfigured.
- newScheduledThreadPool(n): supports delayed tasks and periodic execution through ScheduledExecutorService.
- Additional factories cover single-thread scheduling, work stealing, and Java 21 virtual-thread-per-task execution.
Common mistakes candidates make
- Assuming a fixed pool bounds queued work; its unbounded queue can consume excessive memory under sustained overload.
- Calling a cached pool universally efficient without mentioning its potentially excessive thread creation.
- Treating fixed-rate and fixed-delay scheduling as equivalent, or assuming the same periodic task can overlap with itself.
Likely follow-up questions
How do scheduleAtFixedRate and scheduleWithFixedDelay differ?
Fixed-rate scheduling targets start times based on the initial delay and period, while fixed-delay scheduling measures the delay from one execution's completion to the next execution's start. If a fixed-rate task takes longer than its period, subsequent runs start late but do not overlap; an uncaught exception suppresses future executions for either method.
What happens when tasks arrive faster than a fixed pool can execute them?
Additional tasks accumulate in its unbounded queue, increasing waiting time and potentially exhausting memory. A directly configured ThreadPoolExecutor with a bounded queue and an appropriate rejection policy provides explicit overload handling.
What is the difference between shutdown() and shutdownNow()?
shutdown() rejects new submissions while allowing previously submitted tasks to complete. shutdownNow() attempts to interrupt active tasks and returns tasks that never started, but it cannot force uncooperative tasks to stop; use awaitTermination() when you need to wait for termination.
Interview tip: Start with the four common factories, explain each one's execution and queueing behaviour, then mention overload risks and executor shutdown.
