Mastering Java ThreadPoolExecutor: 4 Rejection Policies Explained & When to Use Them
Java's ThreadPoolExecutor offers four rejection policies—AbortPolicy, DiscardPolicy, DiscardOldestPolicy, and CallerRunsPolicy—each handling task overflow differently; this article explains their behavior, default setting, code examples, and how to configure them via Spring's ThreadPoolTaskExecutor for various business scenarios.
Preface
Thread pools are widely used, but many developers are unfamiliar with their rejection policies.
Four ThreadPoolExecutor Rejection Policies
When the task queue is full and the number of threads reaches maximumPoolSize, new tasks are handled by one of the following policies:
ThreadPoolExecutor.AbortPolicy – discards the task and throws a RejectedExecutionException.
ThreadPoolExecutor.DiscardPolicy – silently discards the task without an exception.
ThreadPoolExecutor.DiscardOldestPolicy – removes the oldest task in the queue and retries the rejected task.
ThreadPoolExecutor.CallerRunsPolicy – the calling thread (the thread that submitted the task) executes the task.
Default Rejection Policy
Inspecting the source of java.util.concurrent.ThreadPoolExecutor shows that the default handler is an AbortPolicy:
/**
* The default rejected execution handler
*/
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();Thus, by default the pool discards the task and throws a RejectedExecutionException. The following test code demonstrates this behavior:
public class ThreadPoolTest {
public static void main(String[] args) {
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
ThreadFactory factory = r -> new Thread(r, "test-thread-pool");
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.SECONDS, queue, factory);
while (true) {
executor.submit(() -> {
try {
System.out.println(queue.size());
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}Running the program confirms that the default policy is AbortPolicy, which throws RejectedExecutionException when the queue is saturated.
Configuring a Custom Rejection Policy
Developers can set a different policy via the overloaded constructor of ThreadPoolExecutor or, when using Spring, through ThreadPoolTaskExecutor:
@Configuration
public class TaskExecutorConfig implements AsyncConfigurer {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 5;
private static final int QUEUE_CAPACITY = 1000;
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
taskExecutor.initialize();
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
return taskExecutor;
}
}By calling setRejectedExecutionHandler on the Spring ThreadPoolTaskExecutor, any of the four policies can be applied.
Policy Scenario Analysis
AbortPolicy
Throws RejectedExecutionException. It is the default and is suitable for critical business logic where immediate failure notification is required.
A handler for rejected tasks that throws a {@code RejectedExecutionException}.
DiscardPolicy
Silently discards the task. Use it for non‑essential work where losing tasks is acceptable.
A handler for rejected tasks that silently discards the rejected task.
DiscardOldestPolicy
Removes the oldest queued task and retries the new one. Choose this when older tasks can be safely dropped in favor of newer ones.
A handler for rejected tasks that discards the oldest unhandled request and then retries {@code execute}, unless the executor is shut down.
CallerRunsPolicy
The submitting thread runs the task itself. This throttles the submission rate and provides a simple back‑pressure mechanism.
A handler for rejected tasks that runs the rejected task directly in the calling thread of the {@code execute} method, unless the executor has been shut down.
Example demonstrating CallerRunsPolicy:
public static void main(String[] args) {
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
ThreadFactory factory = r -> new Thread(r, "test-thread-pool");
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.SECONDS, queue, factory,
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
try {
System.out.println(Thread.currentThread().getName() + ": executing task");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}When the queue is full, the main thread also executes tasks, confirming the behavior of CallerRunsPolicy.
Conclusion
The article introduced the four ThreadPoolExecutor rejection policies and demonstrated how to inspect the default, configure custom handlers, and choose the appropriate policy based on specific business requirements.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Interview Crash Guide
Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.
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.
