skip to content

Executors Factory Methods & Risks

The Executors factory methods are convenient and each hides a hazard: fixed pools queue without bound, cached pools create threads without bound. Interviewers want to hear that you configure a ThreadPoolExecutor with a bounded queue instead.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

What are the main factory methods on java.util.concurrent.Executors, and what kind of thread pool does each create?

level: juniorimportance: must knowfreq 70%

answer

  1. Fixed = n threads + unbounded queue
  2. Cached = unbounded threads + SynchronousQueue
  3. Single = 1 thread, ordered
  4. Scheduled = delayed/periodic
  5. WorkStealing (Java 8) / VirtualThreadPerTask (Java 21)

basics

~10 s

Executors is a helper class with static methods that build ready-made thread pools: newFixedThreadPool (fixed number of threads), newCachedThreadPool (grows/shrinks on demand), newSingleThreadExecutor (one thread), and newScheduledThreadPool (for delayed/repeating tasks).

solid answer

~40 s

Executors is a utility class of static factory methods that return pre-configured ExecutorService instances. newFixedThreadPool(n) keeps exactly n threads with an unbounded queue. newCachedThreadPool() creates threads on demand and reuses idle ones (60s keep-alive), with no fixed ceiling. newSingleThreadExecutor() runs tasks sequentially on one thread, guaranteeing order. newScheduledThreadPool(n)/newSingleThreadScheduledExecutor() support delayed and periodic tasks. Java 8 added newWorkStealingPool() (a ForkJoinPool sized to the CPU count, good for many small independent tasks). Java 21 added newVirtualThreadPerTaskExecutor() (one lightweight virtual thread per task). Each is a thin wrapper that picks sensible defaults around ThreadPoolExecutor (or ForkJoinPool), so you submit Runnable/Callable and don't manage threads yourself.

code

java · 10 lines
java
ExecutorService fixed     = Executors.newFixedThreadPool(4);
ExecutorService cached    = Executors.newCachedThreadPool();
ExecutorService single    = Executors.newSingleThreadExecutor();
ScheduledExecutorService sched = Executors.newScheduledThreadPool(2);
ExecutorService stealing  = Executors.newWorkStealingPool();          // Java 8+
ExecutorService virtual   = Executors.newVirtualThreadPerTaskExecutor(); // Java 21+

Future<Integer> f = fixed.submit(() -> 6 * 7);
System.out.println(f.get());  // 42
fixed.shutdown();

go deeper

for a junior

Name the four classic factories and one-line what each does (fixed count, elastic, single ordered, scheduled). Know you submit Runnable/Callable and get a Future.

for a middle

Add the underlying queue/thread-count for each, the Java 8 work-stealing pool, the Java 21 virtual-thread-per-task executor, and remember to shut pools down.

for a senior

Explain each factory in terms of the ThreadPoolExecutor parameters it sets (core/max/queue/keep-alive) and articulate which workload each suits (CPU vs I/O bound, ordered vs unordered).

for a principal

Frame factory choice as a capacity/back-pressure and failure-isolation decision; know when to bypass the factory entirely for an explicitly configured pool, and the migration story toward virtual threads.

## What problem this solves A **thread** is an independent path of execution the operating system schedules. Creating a thread is relatively expensive (memory for its stack, OS bookkeeping), so creating one per task and throwing it away does not scale. A **thread pool** keeps a set of worker threads alive and feeds them a stream of tasks, amortizing the creation cost and bounding how much work runs at once. In Java the abstraction for "submit work, let a pool run it" is **`ExecutorService`** (an interface). You hand it a **`Runnable`** (a task returning nothing) or a **`Callable<T>`** (a task returning a value), and `submit(...)` gives you back a **`Future<T>`** — a handle you can later call `.get()` on to wait for and retrieve the result. ## What `Executors` is `java.util.concurrent.Executors` is a **utility class** (only static methods) of **factory methods** — methods whose job is to construct and return a ready-made object. Each method returns an `ExecutorService` (or the scheduling sub-interface `ScheduledExecutorService`) already configured with a particular strategy, so you don't have to assemble the lower-level `ThreadPoolExecutor` yourself. ## The factory methods - **`newFixedThreadPool(int n)`** — a pool of exactly `n` worker threads. If more tasks arrive than there are free threads, the extras wait in a queue. The queue here is an **unbounded `LinkedBlockingQueue`** (it can hold an effectively unlimited number of waiting tasks). - **`newCachedThreadPool()`** — starts with zero threads and **creates a new thread for every task that arrives when no idle thread is free**. Idle threads are kept for 60 seconds and then discarded. There is no fixed upper limit on thread count (the nominal max is `Integer.MAX_VALUE`). It uses a `SynchronousQueue`, a queue with **zero capacity** — a task can only be handed off if a thread is ready to take it right now, otherwise a new thread is spun up. - **`newSingleThreadExecutor()`** — exactly one worker thread with an unbounded queue. Because there is only one thread, tasks run **one at a time, in submission order** — useful when you need serialization (e.g., writing to a file). It is also wrapped so the thread count can't be reconfigured. - **`newScheduledThreadPool(int n)`** and **`newSingleThreadScheduledExecutor()`** — return a `ScheduledExecutorService`, which adds `schedule(...)` (run once after a delay), `scheduleAtFixedRate(...)`, and `scheduleWithFixedDelay(...)` (run repeatedly). - **`newWorkStealingPool()`** (Java 8) — returns a `ForkJoinPool` sized by default to the number of available processors, where idle threads "steal" queued tasks from busy threads' deques. Good for a large number of small, independent tasks; does **not** guarantee execution order. - **`newVirtualThreadPerTaskExecutor()`** (Java 21) — creates **one virtual thread per task**. A **virtual thread** is a lightweight thread managed by the JVM (not 1:1 with an OS thread), so you can have millions of them cheaply. There is no pooling of platform threads here — every task gets its own virtual thread that simply ends when the task finishes. Ideal for I/O-bound work with very high concurrency. ## How to use one ```java ExecutorService pool = Executors.newFixedThreadPool(4); Future<Integer> f = pool.submit(() -> 2 + 2); int result = f.get(); // 4 pool.shutdown(); // stop accepting tasks; let running ones finish ``` You call `shutdown()` (graceful) or `shutdownNow()` (attempt to cancel) when done, otherwise the non-daemon worker threads keep the JVM alive. ## Why this matters Knowing which factory maps to which behaviour — fixed parallelism, elastic threads, single-threaded ordering, scheduling, work-stealing, or virtual-thread-per-task — lets you pick the right tool. Crucially, the convenient ones (`newFixedThreadPool`, `newCachedThreadPool`) hide *unbounded* resources, which is the central hazard covered by the follow-on questions.

  • What is the difference between submit() and execute()?
    execute(Runnable) is from the base Executor interface and returns nothing — an uncaught exception propagates to the thread's uncaught-exception handler. submit(...) accepts Runnable or Callable, returns a Future, and captures any thrown exception inside that Future (re-thrown wrapped in ExecutionException on get()).
  • Why do you need to call shutdown()?
    Pool worker threads are non-daemon by default, so they keep the JVM running even after main() finishes. shutdown() stops accepting new tasks and lets queued ones complete; shutdownNow() additionally interrupts running tasks and returns the queued ones.

Think of a restaurant kitchen: a fixed pool is a kitchen with exactly 4 cooks and an order spike just stacks tickets endlessly; a cached pool hires a new cook the instant every order arrives and lays them off when idle; a single-thread pool is one cook doing orders strictly in sequence.

saying these in an interview costs you the question

  • Saying newFixedThreadPool has a bounded queue (its queue is unbounded — that's the OOM risk)
  • Claiming newCachedThreadPool has a fixed maximum thread count
  • Confusing newScheduledThreadPool with a Timer (the Executors version is multi-threaded and resilient to task exceptions)
  • Thinking newWorkStealingPool guarantees task ordering

context

open as a page

Why can a pool from Executors.newFixedThreadPool lead to OutOfMemoryError under load even though the number of threads is fixed?

level: middleimportance: must knowfreq 65%

basics

~20 s

Its threads are fixed, but the queue that holds waiting tasks is unbounded. If tasks arrive faster than the fixed threads can finish them, the queue grows without limit until the JVM runs out of memory.

open as a page

What is the hazard of Executors.newCachedThreadPool() under high or bursty load, and how does it differ from the fixed pool's hazard?

level: middleimportance: should knowfreq 55%

basics

~20 s

A cached pool makes a new thread for almost every incoming task and has no real upper limit, so a burst of work can create thousands of threads at once — exhausting memory or hitting OS limits, instead of just queuing like a fixed pool.

open as a page

What does Executors.newScheduledThreadPool provide, and why is it generally preferred over java.util.Timer?

level: middleimportance: should knowfreq 40%

basics

~20 s

newScheduledThreadPool returns a pool that can run tasks after a delay or repeatedly on a schedule, using multiple threads. It's preferred over Timer because Timer uses a single thread and one task that throws an exception or runs long can break all the others.

open as a page

Why do many style guides and tools (e.g. SonarQube, Google's guidance) discourage the Executors factory methods in favor of constructing ThreadPoolExecutor directly?

level: seniorimportance: should knowfreq 50%

basics

~20 s

The convenient factory methods hide unbounded resources — an unbounded queue (fixed/single) or unbounded threads (cached). Building ThreadPoolExecutor yourself forces you to choose a bounded queue, a thread cap, and what to do when overloaded, so the system fails safely instead of running out of memory.

open as a page