When Does ThreadPoolExecutor’s RejectedExecutionHandler Trigger and How to Use Its Implementations in Java
This article explains the conditions that cause ThreadPoolExecutor’s RejectedExecutionHandler to fire, reviews the built‑in policies such as DiscardOldestPolicy, AbortPolicy, CallerRunsPolicy and DiscardPolicy, provides their source code, and offers practical advice on selecting and avoiding pitfalls when handling rejected tasks in Java concurrency.
ThreadPoolExecutor’s RejectedExecutionHandler is invoked when a task cannot be accepted, either because the executor has been shut down while new tasks are still being submitted or because the pool’s thread count and work queue are full.
The JDK supplies several standard implementations, each with distinct behavior:
DiscardOldestPolicy discards the oldest queued task and retries the new one. Example implementation: public static class DiscardOldestPolicy implements RejectedExecutionHandler { public DiscardOldestPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } } }
AbortPolicy throws a RejectedExecutionException immediately, which is the default policy: public static class AbortPolicy implements RejectedExecutionHandler { public AbortPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); } }
CallerRunsPolicy runs the rejected task in the calling thread when the executor is still active: public static class CallerRunsPolicy implements RejectedExecutionHandler { public CallerRunsPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); } } } This is useful when you want to degrade gracefully to serial execution.
DiscardPolicy simply drops the rejected task without any action: public static class DiscardPolicy implements RejectedExecutionHandler { public DiscardPolicy() { } public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { // no operation – task is discarded } }
When using DiscardPolicy together with Future#get() , the call can block indefinitely; therefore it is recommended to use the timed version Future#get(long timeout, TimeUnit unit) to avoid deadlock.
In practice, developers often extend these handlers to add logging before delegating to the default behavior, and they choose a policy based on business requirements: avoid DiscardOldestPolicy for critical tasks, use CallerRunsPolicy when a fallback to serial execution is acceptable, and ensure proper monitoring of thread‑pool size when applying CallerRunsPolicy .
Summary of recommendations:
Prefer custom handlers that log and then invoke the default implementation.
Never use DiscardPolicy with blocking Future#get() ; switch to the timed get method.
Apply CallerRunsPolicy for performance tuning, but monitor the pool to avoid excessive queuing.
Reserve DiscardOldestPolicy for non‑critical workloads only.
Choose AbortPolicy when the caller must be explicitly aware of rejection and handle it accordingly.
Cognitive Technology Team
Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.