Understanding Java ThreadPoolExecutor Rejection Policies and Their Implementations
This article explains the design of thread pools, when Java's ThreadPoolExecutor triggers rejection policies, details the four built‑in JDK policies and compares several third‑party implementations such as Dubbo, Netty, ActiveMQ and Pinpoint, helping developers choose the appropriate strategy for different scenarios.
Java's ExecutorService and the ThreadPoolExecutor simplify multithreaded programming, but when the pool cannot accept more tasks it applies a rejection policy. The article first introduces the general concept of pool design, illustrating how resources like threads, JDBC connections, or Redis connections are pre‑allocated to avoid the cost of creating them on each request.
Thread pools differ from connection pools because they also contain a blocking queue. A rejection occurs only after the core pool size is exceeded, tasks fill the queue, and finally the maximum pool size is reached; at that point the configured RejectedExecutionHandler is invoked.
JDK Built‑in Rejection Policies
The JDK defines four standard policies:
CallerRunsPolicy
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}When triggered, the submitting thread runs the task directly, suitable for low‑concurrency scenarios where task loss is unacceptable.
AbortPolicy
public static class AbortPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString());
}
}This is the default policy; it throws a RejectedExecutionException , immediately aborting the task.
DiscardPolicy
public static class DiscardPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// silently discard
}
}The task is dropped without any notification, useful only when the work is truly optional.
DiscardOldestPolicy
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}The oldest waiting task is removed from the queue and the new task is submitted, fitting scenarios where newer work supersedes older work.
Third‑Party Implementations
Dubbo – AbortPolicyWithReport
public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
private final String threadName;
private final URL url;
private static volatile long lastPrintTime = 0;
private static Semaphore guard = new Semaphore(1);
public AbortPolicyWithReport(String threadName, URL url) { ... }
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
String msg = String.format("Thread pool is EXHAUSTED! Thread Name: %s, Pool Size: %d ...", ...);
logger.warn(msg);
dumpJStack();
throw new RejectedExecutionException(msg);
}
private void dumpJStack() { /* omitted */ }
}Dubbo logs detailed pool state, dumps the stack trace, and then re‑throws the exception, providing rich diagnostics for production operators.
Netty – NewThreadRunsPolicy
private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
Thread t = new Thread(r, "Temporary task executor");
t.start();
} catch (Throwable e) {
throw new RejectedExecutionException("Failed to start a new thread", e);
}
}
}Similar to CallerRunsPolicy but creates a fresh thread, allowing higher throughput at the cost of extra thread creation.
ActiveMQ – Timed Queue Offer
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
executor.getQueue().offer(r, 60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker");
}
throw new RejectedExecutionException("Timed Out while attempting to enqueue Task.");
}
};ActiveMQ attempts to enqueue the task for up to one minute before giving up, favoring eventual execution when possible.
Pinpoint – RejectedExecutionHandlerChain
public class RejectedExecutionHandlerChain implements RejectedExecutionHandler {
private final RejectedExecutionHandler[] handlerChain;
public static RejectedExecutionHandler build(List
chain) {
Objects.requireNonNull(chain, "handlerChain must not be null");
return new RejectedExecutionHandlerChain(chain.toArray(new RejectedExecutionHandler[0]));
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
for (RejectedExecutionHandler h : handlerChain) {
h.rejectedExecution(r, executor);
}
}
}Pinpoint composes multiple handlers into a chain, executing each in order when a rejection occurs.
In conclusion, the article walks through the pool design philosophy, explains when rejection happens, enumerates JDK's four built‑in policies, and surveys four open‑source implementations, giving readers a comprehensive view to select and possibly extend rejection strategies for their own Java applications.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.