skip to content

Work Queues & Rejection Policies

The queue you choose determines whether the pool grows, buffers or hands off directly, and the rejection handler determines what happens when it is saturated. CallerRunsPolicy as a backpressure mechanism is the answer interviewers are usually fishing for.

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

questions

5

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%

answer

  1. Queue = buffer between submitters and worker threads
  2. Rule: core -> queue -> max -> reject
  3. Unbounded queue => max ignored, never rejects
  4. Default handler = AbortPolicy throws RejectedExecutionException
  5. Rejection is delegated to a RejectedExecutionHandler

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).

solid answer

~40 s

A ThreadPoolExecutor decouples task submission from execution: submitted tasks are buffered in a BlockingQueue until a worker thread is free. The submission rule is: if fewer than corePoolSize threads exist, start a new thread; otherwise try to enqueue the task; if the queue is full, start a thread up to maximumPoolSize; if that also fails, reject the task. Rejection is delegated to a RejectedExecutionHandler. The default handler is AbortPolicy, which throws a RejectedExecutionException to the submitting caller. So the queue provides buffering and backpressure, and the rejection policy decides what happens when the buffer plus pool are saturated. The queue type chosen (bounded vs unbounded) directly determines whether rejection can ever happen at all.

go deeper

for a junior

Can state that the queue buffers tasks and that a saturated pool rejects, with the default throwing RejectedExecutionException.

for a middle

Can recite the core -> queue -> max -> reject order and explain why an unbounded queue disables maximumPoolSize.

for a senior

Can reason about how queue choice plus rejection policy together define the system's backpressure and failure mode under load.

for a principal

Frames queue/rejection as a capacity-planning and overload-control decision: bounded queues for predictable memory, explicit rejection or CallerRuns for backpressure, and ties it to SLOs and graceful degradation.

## What an executor and its queue are A **thread pool** is a fixed or bounded set of reusable worker threads that pull tasks off a shared queue and run them, instead of creating one new OS thread per task. In Java the central implementation is `java.util.concurrent.ThreadPoolExecutor` (the concrete class behind most `Executors.newXxx` factories). A **task** is a unit of work — a `Runnable` or `Callable` — that you hand to the executor via `execute(...)` or `submit(...)`. The **work queue** is a `BlockingQueue<Runnable>` that the executor holds. "Blocking" means a worker thread that calls `take()` on an empty queue will park (wait) until a task appears, rather than spinning or returning null. The queue is the buffer between *producers* (threads submitting tasks) and *consumers* (the pool's worker threads). ## The submission algorithm (why the queue matters) When you submit a task, `ThreadPoolExecutor` follows a precise three-step rule using three parameters — `corePoolSize` (the baseline number of threads kept alive), `maximumPoolSize` (the ceiling), and the queue: 1. If the number of running threads is **less than `corePoolSize`**, start a brand-new worker thread to run this task immediately — even if other threads are idle. 2. Otherwise, try to **enqueue** the task onto the work queue (`offer`). 3. If the queue **refuses** the task (it is full), try to start a new thread, up to `maximumPoolSize`. 4. If the pool is already at `maximumPoolSize` and the queue is full, the task is **rejected**. The surprising consequence: the pool only grows past `corePoolSize` when the *queue is full*. An **unbounded** queue (one that never refuses) therefore means step 3 never fires — `maximumPoolSize` is effectively ignored and rejection can never happen. ## Rejection **Rejection** is what the executor does when it can neither run nor buffer a task. It calls the configured `RejectedExecutionHandler.rejectedExecution(task, executor)`. The built-in handlers: - **`AbortPolicy`** — the *default* — throws `RejectedExecutionException` (an unchecked `RuntimeException`) back to the caller. - **`CallerRunsPolicy`** — runs the task directly on the calling thread (provides natural backpressure). - **`DiscardPolicy`** — silently drops the task. - **`DiscardOldestPolicy`** — drops the oldest queued task and retries the new one. Because `AbortPolicy` is the default, an unconfigured `ThreadPoolExecutor` that saturates will surface a `RejectedExecutionException` to whoever submitted the task — a fail-fast signal, not silent data loss. ## Why this design exists Decoupling submission from execution lets the system absorb bursts (queue), bound resource use (max threads + bounded queue), and choose an explicit overload strategy (rejection policy) rather than letting unbounded memory growth crash the JVM. A junior should remember: queue = buffer, full pool + full queue = rejection, default = throw.

  • With an unbounded LinkedBlockingQueue, does maximumPoolSize have any effect?
    No. Since the queue never refuses a task, the executor never reaches the 'queue full -> add thread' step, so the pool never grows beyond corePoolSize and maximumPoolSize is effectively dead configuration.
  • Which method receives the rejection — submit() or the handler?
    The executor invokes RejectedExecutionHandler.rejectedExecution(task, executor); with the default AbortPolicy that throws RejectedExecutionException, which propagates out of the submit()/execute() call to the caller.

saying these in an interview costs you the question

  • Thinking the pool grows to maximumPoolSize before filling the queue (it's the opposite: queue fills first)
  • Believing an unbounded queue ever triggers rejection
  • Assuming rejection silently drops the task by default (default actually throws)
  • Confusing RejectedExecutionException with InterruptedException

context

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

Why is Executors.newFixedThreadPool considered risky in production, and how would you build a safer executor for a high-throughput service?

level: seniorimportance: should knowfreq 47%

basics

~20 s

newFixedThreadPool uses an unbounded LinkedBlockingQueue, so under overload the queue grows without limit and can run the JVM out of memory instead of rejecting work. A safer setup constructs a ThreadPoolExecutor directly with a bounded queue and an explicit rejection policy.

open as a page

What subtle failure modes can CallerRunsPolicy introduce, and when is it the wrong backpressure choice?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

CallerRunsPolicy runs the rejected task on the submitting thread. If that thread is a critical or shared thread (like an event loop or another pool's worker), running long tasks there can stall the system or even deadlock. It also makes task latency unpredictable for the unlucky caller.

open as a page