RejectedExecutionHandler is the callback interface a ThreadPoolExecutor invokes when it cannot accept a new task — typically because both its worker threads and its task queue are already full — letting you define custom behavior instead of the default hard failure.
Key Points: • The four built-in policies are AbortPolicy (throws RejectedExecutionException, the default), CallerRunsPolicy (runs the task on the submitting thread), DiscardPolicy (silently drops it), and DiscardOldestPolicy (drops the oldest queued task and retries). • You implement it by overriding rejectedExecution(Runnable r, ThreadPoolExecutor executor) with custom logic such as logging, alerting, retry queues, or routing to a fallback executor. • CallerRunsPolicy is a common backpressure technique — it slows down the producer thread by making it do the work itself, which naturally throttles submission rate. • Custom handlers are set either through the ThreadPoolExecutor constructor or via setRejectedExecutionHandler(). • Choosing the right policy depends on whether losing a task is acceptable, or whether backpressure/retry is more appropriate for the workload.
Example: A high-throughput ingestion service might implement a custom handler that pushes rejected tasks onto a dead-letter queue for later reprocessing instead of silently dropping them or throwing an unhandled exception that could crash the caller.
Code Example:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, 8, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
(r, exec) -> {
// custom handling, e.g. log and route to a fallback
log.warn("Task rejected, running on caller thread");
r.run();
});Interview Tip: A concise interview answer is:
"RejectedExecutionHandler defines what happens when a ThreadPoolExecutor can't accept a task because its queue and pool are both full. The built-in options are abort, caller-runs, discard, and discard-oldest, and you can implement your own to log, retry, or route overflow tasks instead of just failing outright."