skip to content

When a ThreadPoolExecutor's queue and pool are full, what happens to a submitted Command, and how is that behavior controlled?

level: seniorimportance: should knowfreq 38%

answer

  1. Reject only when bounded queue full AND pool at max
  2. Default AbortPolicy → RejectedExecutionException
  3. CallerRunsPolicy = backpressure (runs on caller's thread)
  4. Discard / DiscardOldest = drop work
  5. newFixedThreadPool uses unbounded queue → never rejects, can OOM

basics

~20 s

If all threads are busy and the work queue is full, the executor can't accept the task, so it applies a rejection policy. The default policy throws RejectedExecutionException. You can choose other policies, like running the task on the caller's thread or silently discarding it.

solid answer

~50 s

A ThreadPoolExecutor accepts a command only if a thread is free, or the work queue has room, or the pool can grow up to maximumPoolSize. When all of those are exhausted — bounded queue full and pool at max — the task is rejected and the executor's RejectedExecutionHandler runs. The JDK provides four: AbortPolicy (the default) throws RejectedExecutionException; CallerRunsPolicy runs the task on the submitting thread, which throttles producers by slowing them down; DiscardPolicy silently drops the new task; DiscardOldestPolicy drops the head of the queue and retries. This matters because an unbounded queue (the default for newFixedThreadPool) never rejects and can grow until OutOfMemoryError, so rejection only bites with a bounded queue. Choosing a bounded queue plus an explicit rejection policy is how you give a system backpressure instead of silently piling up commands.

go deeper

for a junior

Knows that an overloaded executor may reject a task and the default throws an exception.

for a middle

Explains the queue-then-grow-then-reject acceptance order and names the four rejection policies.

for a senior

Connects rejection to backpressure: chooses bounded queues, explains CallerRunsPolicy throttling, and warns that newFixedThreadPool's unbounded queue risks OOM rather than rejecting.

for a principal

Designs end-to-end overload behavior — queue sizing, custom rejection handlers, load shedding vs throttling, and SLA/latency implications — and reasons about it as a system-level capacity and resilience decision.

## The acceptance decision A `ThreadPoolExecutor` is configured with: `corePoolSize`, `maximumPoolSize`, a **work queue** (`BlockingQueue<Runnable>`), and a **RejectedExecutionHandler**. When you submit a command, it decides in this order: 1. If fewer than `corePoolSize` threads are running → **start a new core thread** to run it. 2. Else, try to **enqueue** the task in the work queue. 3. If the queue is **full**, and the pool has fewer than `maximumPoolSize` threads → **start a new (non-core) thread** to run it. 4. If the queue is full **and** the pool is at `maximumPoolSize` → the task is **rejected**: the `RejectedExecutionHandler.rejectedExecution(...)` runs. So rejection happens only when *both* the queue and the pool are saturated. ## The queue choice changes everything The `Executors` factory methods hide the queue: - `newFixedThreadPool(n)` uses an **unbounded `LinkedBlockingQueue`** → step 2 *always* succeeds, the pool never grows past core, and **tasks are never rejected** — instead they accumulate without limit, risking **OutOfMemoryError**. - `newCachedThreadPool()` uses a **`SynchronousQueue`** (zero capacity) → every task needs an immediately-available thread; the pool grows nearly unbounded, which can exhaust threads. To get *real* rejection/backpressure you construct a `ThreadPoolExecutor` directly with a **bounded** queue (e.g. `new ArrayBlockingQueue<>(1000)`). ## The four built-in rejection policies - **`AbortPolicy`** (default): throws `RejectedExecutionException` on submission — the caller learns immediately and decides what to do. - **`CallerRunsPolicy`**: runs the task **in the submitting thread**. The submitter is busy doing the work, so it can't submit more — a simple, elegant **backpressure** mechanism that throttles producers under load. - **`DiscardPolicy`**: silently **drops** the rejected task. Dangerous (lost work, no signal) unless loss is acceptable. - **`DiscardOldestPolicy`**: drops the **oldest queued** task (the head) and retries the new one — favors freshness over completeness. You can also implement `RejectedExecutionHandler` yourself (e.g. log, block-and-retry with `queue.put`, or route to a fallback). ```java ThreadPoolExecutor pool = new ThreadPoolExecutor( 4, 8, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), new ThreadPoolExecutor.CallerRunsPolicy()); ``` ## After shutdown Once you call `shutdown()`/`shutdownNow()`, any **new** submission is also rejected via the same handler (default: throws `RejectedExecutionException`), because the executor no longer accepts work. ## Why a senior cares: backpressure The deep point is **backpressure**. A command is just an object; without limits, a fast producer can enqueue commands faster than the pool drains them, and an unbounded queue turns that into unbounded memory growth and rising latency. A **bounded queue + a deliberate rejection policy** converts overload into a *visible, bounded* condition (throw, throttle, shed load) rather than a silent heap blow-up. Picking the policy is a product decision: fail fast (Abort), self-throttle (CallerRuns), or shed load (Discard variants). ## Key terms - **corePoolSize / maximumPoolSize**: the minimum kept-alive threads and the hard ceiling on threads. - **BlockingQueue**: the holding area for pending commands; bounded vs unbounded is the key knob. - **RejectedExecutionHandler**: the strategy invoked when a task can't be accepted. - **Backpressure**: mechanisms that slow or bound producers so they can't overwhelm consumers. - **SynchronousQueue**: a zero-capacity queue requiring a waiting consumer for every producer.

  • Why does CallerRunsPolicy act as backpressure?
    Because the rejected task runs synchronously on the submitting thread, that thread is occupied doing the work and cannot submit more tasks until it finishes. This naturally slows the producer to the pool's drain rate instead of letting the queue grow.
  • Why can newFixedThreadPool(n) lead to OutOfMemoryError under sustained overload?
    It uses an unbounded LinkedBlockingQueue, so tasks are never rejected; if producers outpace the n workers, the queue grows without limit until the heap is exhausted. A bounded queue plus a rejection policy prevents this.

saying these in an interview costs you the question

  • Thinking newFixedThreadPool rejects tasks under load — its unbounded queue never rejects (it OOMs)
  • Believing the pool grows to maximumPoolSize before the queue fills (it grows only after the queue is full)
  • Assuming DiscardPolicy signals the loss — it is silent
  • Forgetting that submissions after shutdown are also rejected

context