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.

Programmer DD
Programmer DD
Programmer DD
Mastering Java ThreadPool Rejection Policies: When and How to Choose

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).

ThreadPool rejection timing diagram
ThreadPool rejection timing diagram

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaThreadPoolExecutorRejectionPolicy
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.