skip to content

Executor Framework

The Executor framework: submitting tasks to managed thread pools instead of creating threads by hand, plus queueing, rejection, lifecycle and sizing. This is the most practically relevant concurrency area in backend interviews because pool misconfiguration is a real production failure mode.

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

explore

questions

page 1 of 2

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

What is the Executor interface, and what problem does it solve compared to creating threads directly?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Executor is a tiny interface with one method, execute(Runnable). You hand it a task to run; it decides how and when to run it. This separates submitting work from creating threads, so you don't call new Thread(...).start() everywhere.

open as a page

What is the difference between Callable<V> and Runnable, and when would you choose one over the other?

level: juniorimportance: must knowfreq 78%

basics

~10 s

Runnable.run() returns nothing and can't throw checked exceptions. Callable<V>.call() returns a value of type V and can throw checked exceptions. Use Callable when the task produces a result you need back.

open as a page

How do getActiveCount(), getPoolSize(), and getLargestPoolSize() differ on a ThreadPoolExecutor, and what does each tell you about pool health?

level: juniorimportance: must knowfreq 62%

basics

~10 s

getPoolSize() is how many threads exist right now, getActiveCount() is how many are currently running a task, and getLargestPoolSize() is the highest pool size ever reached. Together they show how busy the pool is.

open as a page

What role does the work queue play in a Java ThreadPoolExecutor, and what is the default rejection behavior when work can no longer be accepted?

level: juniorimportance: must knowfreq 62%

basics

~20 s

The work queue holds tasks that have been submitted but no thread is free to run yet. When the pool and queue are both full and no more work can be taken, the executor rejects new tasks; by default it throws RejectedExecutionException (AbortPolicy).

open as a page

What is a ScheduledExecutorService and how do you use it to run a task after a delay or on a repeating schedule?

level: juniorimportance: must knowfreq 62%

basics

~10 s

It is an executor that runs tasks later or repeatedly. You create one with Executors.newScheduledThreadPool(n), then call schedule() for a one-shot delay, or scheduleAtFixedRate/scheduleWithFixedDelay to repeat. Always shut it down when done.

open as a page

What are the constructor parameters of a ThreadPoolExecutor, and what does each one control?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A ThreadPoolExecutor reuses a set of worker threads to run tasks. Its main settings are: core pool size (threads kept alive), maximum pool size (most threads allowed), keep-alive time (how long extra threads idle before stopping), and a work queue that holds waiting tasks.

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 difference between Executor.execute(Runnable) and ExecutorService.submit(...)? Pay special attention to return values and exception handling.

level: middleimportance: must knowfreq 80%

basics

~20 s

execute runs a Runnable and returns nothing (fire-and-forget); if it throws, the exception goes to the thread's uncaught-exception handler. submit accepts a Runnable or Callable and returns a Future you can wait on; any exception is captured inside that Future and re-thrown only when you call get().

open as a page

ExecutorService extends Executor with a lifecycle. Walk through shutting one down correctly, contrasting shutdown(), shutdownNow(), and awaitTermination().

level: middleimportance: must knowfreq 68%

basics

~20 s

ExecutorService adds a lifecycle on top of Executor. shutdown() stops accepting new tasks but lets queued ones finish. shutdownNow() tries to stop immediately and returns the unstarted tasks. awaitTermination() blocks until everything finishes or a timeout passes. You must shut it down, or its threads keep the JVM alive.

open as a page

What is the difference between shutdown() and shutdownNow() on an ExecutorService, and what does each return or do to in-flight tasks?

level: middleimportance: must knowfreq 78%

basics

~10 s

shutdown() stops accepting new tasks but lets already-submitted ones finish. shutdownNow() tries to stop running tasks by interrupting them and returns the list of tasks that never started.

open as a page

How do you retrieve the result of an asynchronous task with Future, and what are the semantics of get(), the timed get(), isDone(), isCancelled(), and cancel()?

level: middleimportance: must knowfreq 80%

basics

~20 s

Future is a handle to a task's eventual result. future.get() blocks until the task finishes and returns the value (or throws if it failed). The timed get() waits at most a duration. isDone()/isCancelled() check state; cancel() tries to stop it.

open as a page

How do you observe the backlog and throughput of a ThreadPoolExecutor using its queue and getCompletedTaskCount()/getTaskCount()?

level: middleimportance: must knowfreq 55%

basics

~10 s

Call getQueue().size() to see how many tasks are waiting (the backlog), getCompletedTaskCount() to see how many have finished, and getTaskCount() for the total ever scheduled. A growing queue means the pool can't keep up.

open as a page

Walk through the built-in RejectedExecutionHandlers (AbortPolicy, CallerRunsPolicy, DiscardPolicy, DiscardOldestPolicy). What does each do and when would you choose each?

level: middleimportance: must knowfreq 55%

basics

~20 s

AbortPolicy throws an exception (the default). CallerRunsPolicy runs the task on the thread that submitted it. DiscardPolicy silently drops the task. DiscardOldestPolicy drops the oldest queued task and tries to add the new one. You pick based on whether you want failure, backpressure, or silent dropping.

open as a page

Compare LinkedBlockingQueue, ArrayBlockingQueue, SynchronousQueue, and PriorityBlockingQueue as the work queue of a thread pool. How does each affect pool growth and backpressure?

level: middleimportance: must knowfreq 58%

basics

~20 s

LinkedBlockingQueue is usually unbounded, so the pool stays at core size and can grow memory unbounded. ArrayBlockingQueue is bounded, so it can trigger pool growth and rejection. SynchronousQueue holds nothing and hands tasks directly to a thread, forcing new threads. PriorityBlockingQueue orders tasks by priority and is unbounded.

open as a page

What is the difference between scheduleAtFixedRate and scheduleWithFixedDelay, and how does task duration affect each?

level: middleimportance: must knowfreq 70%

basics

~20 s

FixedRate measures the period from the start of one run to the start of the next, so it tries to run every N units. FixedDelay measures the gap from the end of one run to the start of the next. If a task runs longer than the rate period, fixedRate runs do not overlap (they queue back-to-back) while fixedDelay always leaves the full gap.

open as a page

Walk through exactly how ThreadPoolExecutor decides whether to create a thread, enqueue a task, or reject it when execute() is called.

level: middleimportance: must knowfreq 75%

basics

~20 s

If fewer than core threads exist, it starts a new thread. Otherwise it tries to put the task in the queue. If the queue is full, it starts more threads up to the maximum. If the queue is full and threads are at the maximum, the task is rejected.

open as a page

How do you size a thread pool for CPU-bound versus I/O-bound workloads, and what is the reasoning behind the formulas?

level: seniorimportance: must knowfreq 72%

basics

~20 s

For CPU-heavy work use about (number of cores + 1) threads, because the CPU is the bottleneck. For I/O-heavy work use more threads, since each thread spends most of its time waiting rather than computing.

open as a page

What happens to a periodic task scheduled on a ScheduledExecutorService if its body throws an uncaught exception, and how do you handle it safely?

level: seniorimportance: must knowfreq 66%

basics

~20 s

If a periodic task throws an uncaught exception, that task is silently cancelled — it never runs again — and you usually see no log. The fix is to wrap the task body in try/catch so the exception never escapes, or inspect the returned ScheduledFuture.

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

Explain awaitTermination, isShutdown, and isTerminated. How do you correctly wait for an ExecutorService to finish?

level: middleimportance: should knowfreq 60%

basics

~10 s

isShutdown() is true once you've called shutdown/shutdownNow. isTerminated() is true only after all tasks have actually finished. awaitTermination() blocks until termination or a timeout, so you call it to wait.

open as a page

What does ScheduledFuture give you, and how do you cancel, query, or read the result of scheduled tasks?

level: middleimportance: should knowfreq 48%

basics

~20 s

Every schedule call returns a ScheduledFuture. For a one-shot Callable you read its result with get(). For any task you can cancel it with cancel(mayInterruptIfRunning), and check getDelay() to see how long until the next run. Keep the reference if you ever want to stop the task.

open as a page

What are the built-in RejectedExecutionHandler policies, when does rejection occur, and how would you choose between them?

level: middleimportance: should knowfreq 55%

basics

~20 s

Rejection happens when the queue is full and the pool is already at its maximum size (or after shutdown). The built-in policies are: throw an exception (AbortPolicy, the default), run the task on the calling thread (CallerRunsPolicy), silently drop it (DiscardPolicy), or drop the oldest queued task (DiscardOldestPolicy).

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

Explain invokeAll and invokeAny on ExecutorService: their blocking and return semantics, how they handle exceptions and cancellation, and when you'd reach for each.

level: seniorimportance: should knowfreq 52%

basics

~20 s

Both take a collection of Callables. invokeAll runs them all and blocks until every one finishes, returning a list of Futures in the same order. invokeAny runs them and blocks until the first one succeeds, returning that single result and cancelling the rest. Use invokeAll for fan-out/gather; invokeAny when any one good answer suffices.

open as a page

Why might tasks fail to stop on shutdownNow(), and how do you write tasks that respond to cancellation correctly?

level: seniorimportance: should knowfreq 55%

basics

~20 s

shutdownNow() only interrupts the worker threads — it sets a flag. A task that never checks that flag or never calls a method that reacts to it just keeps running. You must check the interrupt flag in loops and not swallow InterruptedException.

open as a page

What problem does ExecutorCompletionService solve, and how does it differ from collecting a List<Future> and calling get() on each?

level: seniorimportance: should knowfreq 58%

basics

~20 s

ExecutorCompletionService hands you results in the order tasks finish, not the order you submitted them. You call take() to get the next completed Future, so a slow task can't block you from processing fast ones that already finished.

open as a page

Walk through correct exception handling and cooperative cancellation when working with Callable tasks and their Futures. What are the common mistakes?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Wrap get() to catch InterruptedException and ExecutionException, and read the real failure from getCause(). To stop a task, call cancel(true); the task only stops if its code checks the interrupt flag or calls interruptible methods. Always restore the interrupt flag.

open as a page

What are the beforeExecute(Thread, Runnable) and afterExecute(Runnable, Throwable) hooks on ThreadPoolExecutor, and how do you use them for instrumentation and per-task setup/teardown?

level: seniorimportance: should knowfreq 44%

basics

~20 s

They are overridable methods that run on the worker thread right before and right after each task. You subclass ThreadPoolExecutor and override them to time tasks, log, or set up and clean up thread-local context around every task.

open as a page

showing 1–30 of 39