Mastering Java ThreadPool Rejection Policies: When and How to Choose
This article explains the design principles of thread pools, the conditions that trigger rejection policies in Java's ThreadPoolExecutor, details the four built‑in JDK strategies, and reviews several third‑party implementations with their appropriate use‑cases, helping developers select the right policy for their workloads.
Introduction
When talking about Java thread pools, the most familiar API is the ExecutorService interface introduced in JDK 1.5 under java.util.concurrent. Whether you use a fixed or cached thread pool, the underlying implementation is ThreadPoolExecutor, which follows a typical pool‑based design and therefore involves rejection policies when the pool cannot accommodate more tasks.
Pool Design Philosophy
Pool design is not a new concept. Common examples include Java thread pools, JDBC connection pools, and Redis connection pools. The idea is to pre‑allocate resources to avoid the overhead of creating threads or remote connections on each request, similar to a cafeteria where food is prepared in advance. Key pool attributes such as core size, maximum size, and active size map directly to the properties of Java thread pools and database connection pools.
When ThreadPool Triggers Rejection Policies
Unlike data‑source connection pools that trigger rejection when the number of connections exceeds the maximum, a thread pool also has a blocking queue to buffer tasks. The rejection occurs when the number of submitted tasks exceeds corePoolSize and fills the queue, after which the pool checks if the total tasks surpass maxPoolSize. If they do, the rejection policy is applied. In short, rejection is triggered when submittedTasks > (maxPoolSize + queueCapacity).
Four Built‑in JDK Rejection Policies
RejectedExecutionHandler Interface
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}CallerRunsPolicy
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}Function: If the pool is not shut down, the submitting thread runs the task.
Use case: Suitable for scenarios where failure is not acceptable, performance requirements are low, and the concurrency level is small.
AbortPolicy
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());
}
}Function: Throws a RejectedExecutionException, aborting the current execution flow.
Use case: Default policy in JDK; suitable when you want the failure to be explicit. Note that the default queue in ExecutorService is unbounded, so rejection may never occur unless a custom bounded queue is used.
DiscardPolicy
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { }
}Function: Silently discards the task without any action.
Use case: When the task is non‑essential and can be ignored.
DiscardOldestPolicy
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}Function: Removes the oldest task in the queue (if the pool is not shut down) and then attempts to execute the new task.
Use case: Useful when newer tasks have higher priority, such as updating a message that supersedes an older pending message.
Third‑Party Implementations
Dubbo’s AbortPolicyWithReport
public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);
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) {
this.threadName = threadName;
this.url = url;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
String msg = String.format("Thread pool is EXHAUSTED! Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d), Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
url.getProtocol(), url.getIp(), url.getPort());
logger.warn(msg);
dumpJStack();
throw new RejectedExecutionException(msg);
}
private void dumpJStack() { /* omitted */ }
}Dubbo logs detailed pool parameters, dumps the thread stack, and then throws the exception, providing clear diagnostics for operators.
Netty’s NewThreadRunsPolicy
private static final class NewThreadRunsPolicy implements RejectedExecutionHandler {
NewThreadRunsPolicy() { super(); }
@Override
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 new temporary thread, making it suitable for high‑performance scenarios while still avoiding task loss.
ActiveMQ’s Timeout‑Based Policy
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(final Runnable r, final 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.");
}
}This policy attempts to re‑queue the task for up to one minute; if it still cannot be accepted, it throws an exception.
Pinpoint’s RejectedExecutionHandlerChain
public class RejectedExecutionHandlerChain implements RejectedExecutionHandler {
private final RejectedExecutionHandler[] handlerChain;
public static RejectedExecutionHandler build(List<RejectedExecutionHandler> chain) {
Objects.requireNonNull(chain, "handlerChain must not be null");
return new RejectedExecutionHandlerChain(chain.toArray(new RejectedExecutionHandler[0]));
}
private RejectedExecutionHandlerChain(RejectedExecutionHandler[] handlerChain) {
this.handlerChain = Objects.requireNonNull(handlerChain, "handlerChain must not be null");
}
@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, offering flexible extensibility.
Conclusion
The article introduced the pool design concept, explained when a thread pool triggers rejection, defined the RejectedExecutionHandler interface, described JDK’s four built‑in policies, and presented four third‑party implementations with their respective scenarios. Readers should now have a deeper understanding of Java thread‑pool rejection strategies and be able to choose the most appropriate one for their specific use cases.
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
