Walk through the built-in RejectedExecutionHandlers (AbortPolicy, CallerRunsPolicy, DiscardPolicy, DiscardOldestPolicy). What does each do and when would you choose each?
answer
- Abort = throw (default, fail-fast)
- CallerRuns = submitter runs it => backpressure
- Discard = silent drop
- DiscardOldest = drop queue head, retry new
- Handler only fires when the queue is bounded/saturated
basics
~20 sAbortPolicy 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.
solid answer
~50 sWhen a saturated pool can't accept a task it invokes the configured RejectedExecutionHandler. AbortPolicy, the default, throws RejectedExecutionException so the submitter learns immediately — good for fail-fast correctness. CallerRunsPolicy runs the rejected task synchronously on the submitting thread; because that thread is now busy executing, it stops submitting more work, which naturally throttles producers — a simple, effective backpressure mechanism. DiscardPolicy silently drops the task with no error — only acceptable when losing work is genuinely fine (e.g. best-effort metrics). DiscardOldestPolicy evicts the head of the queue (the longest-waiting task) and retries the new one, favoring fresh work over stale — but it can drop important tasks and behaves oddly with priority queues. You can also implement RejectedExecutionHandler yourself to log, meter, persist to a dead-letter store, or block. Choose by overload semantics: fail fast (Abort), throttle (CallerRuns), or shed load (Discard variants).
go deeper
Can list the four handlers and that the default throws.
Can explain each handler's behavior and pick one for fail-fast vs backpressure vs load-shedding.
Can reason about CallerRunsPolicy's caller-thread risks, write a custom handler (log/meter/dead-letter), and avoid DiscardOldest with priority queues.
Designs overload policy holistically — combining bounded queues, handler choice, retries/dead-letter, and observability — and weighs caller-runs deadlock and latency-stall risks across interacting pools.
## The rejection hook When a `ThreadPoolExecutor` cannot run a task immediately (no idle thread) and cannot queue it (queue full or refusing) and cannot grow (already at `maximumPoolSize`) — or when the executor has been shut down — it calls: ``` handler.rejectedExecution(Runnable task, ThreadPoolExecutor executor) ``` `RejectedExecutionHandler` is a single-method interface. The JDK ships four implementations as static nested classes of `ThreadPoolExecutor`. ### AbortPolicy (the default) Throws a `RejectedExecutionException` — an unchecked `RuntimeException`. The exception propagates out of `execute()`/`submit()` to the caller. - **Pros:** fail-fast and loud; you never silently lose work, and the caller can react (retry, alert, shed). - **Cons:** the caller must handle the exception; an unhandled one can crash a request thread. - **Use when:** correctness matters and dropped work is unacceptable; you want overload to be visible. ### CallerRunsPolicy Does **not** throw and does **not** drop. Instead it runs the task's `run()` **directly on the calling (submitting) thread** — provided the executor isn't shut down. The clever effect: while the submitter is busy executing the rejected task, it **cannot submit new tasks**, so the production rate is throttled to match consumption. This is a built-in **backpressure / flow-control** mechanism. - **Pros:** no task loss, automatic throttling, no extra threads. - **Cons:** the submitting thread (possibly an event loop, HTTP acceptor, or another pool's thread) is blocked running the work, which can stall latency-sensitive paths or, in pathological setups, deadlock if that thread is itself needed to drain the pool. - **Use when:** you want graceful degradation under load and the caller can afford to do the work. ### DiscardPolicy **Silently drops** the rejected task. No exception, no logging, the task simply never runs (its `Future`, if any, never completes normally). - **Pros:** trivially sheds load. - **Cons:** silent data loss — extremely easy to mask a real problem; futures hang forever. - **Use when:** the work is genuinely best-effort and losing it has no consequence (e.g. dropping a redundant cache-warm task). ### DiscardOldestPolicy If the executor isn't shut down, it **polls (removes) the head of the work queue** — the oldest, longest-waiting task — discards it, and then **retries** `execute(task)` for the new one. It prefers newer work over stale. - **Pros:** keeps the freshest tasks; useful when newer data supersedes older (e.g. latest sensor reading). - **Cons:** silently drops a task that was first in line (possibly the most important / most-aged), and with a `PriorityBlockingQueue` the 'head' is the *highest-priority* element, so it would discard your most important task — usually wrong there. - **Use when:** newest-wins semantics and FIFO staleness is the real enemy. ## Custom handlers Because `RejectedExecutionHandler` is a one-method interface, you can supply your own to: log + meter rejections, push to a dead-letter queue or persistent store for later retry, apply a bounded blocking put (`queue.put`) to throttle without busy-running the caller, or trigger autoscaling. A common production pattern is a logging+metrics handler so overload is observable rather than silent. ## Choosing - **Fail fast / never lose work:** AbortPolicy (or a custom handler that retries/persists). - **Throttle producers, no loss:** CallerRunsPolicy. - **Shed load, loss acceptable:** DiscardPolicy (any age) or DiscardOldestPolicy (drop stale). Always pair an unbounded queue's *absence* (i.e. use a bounded queue) with a deliberate handler — the handler only ever runs when the queue can actually fill.
- How exactly does CallerRunsPolicy create backpressure?It runs the rejected task on the producer's own thread. While that thread is busy executing the task, it is not looping to submit more work, so the submission rate is forced down to the rate the pool can drain — throttling producers to consumers without dropping anything.
- Why can DiscardOldestPolicy be dangerous with a PriorityBlockingQueue?DiscardOldestPolicy removes the head of the queue. In a PriorityBlockingQueue the head is the highest-priority element, so the policy would silently discard your most important task instead of a stale low-priority one — the opposite of the intent.
saying these in an interview costs you the question
- Saying CallerRunsPolicy uses a new thread (it reuses the submitting thread)
- Claiming DiscardPolicy logs or throws (it is fully silent)
- Recommending DiscardOldestPolicy with a PriorityBlockingQueue (it drops the highest-priority task)
- Believing a rejection handler runs even with an unbounded queue (it never gets the chance)