Mastering Java ThreadPoolExecutor Rejection Policies: When and How to Use Them

This article explains the design principles of thread pools, when Java's ThreadPoolExecutor triggers rejection policies, details the four built‑in JDK policies with code examples, and explores additional strategies from Dubbo, Netty, ActiveMQ and Pinpoint, helping developers choose the right approach for their workloads.

Programmer DD
Programmer DD
Programmer DD
Mastering Java ThreadPoolExecutor Rejection Policies: When and How to Use Them

Preface

When discussing Java thread pools, the most familiar API is ExecutorService, introduced in JDK 1.5 under java.util.concurrent. Whether you use a fixed or cached thread pool, the underlying implementation is ThreadPoolExecutor. ThreadPoolExecutor follows a typical pooled design: a fixed size pool, a queue for pending tasks, and a rejection policy that activates when the pool cannot accept more work.

Pool Design Philosophy

Pooling is common in Java thread pools, JDBC connection pools, Redis connection pools, etc. It pre‑allocates resources to avoid the overhead of creating them on each request, similar to a cafeteria where meals are prepared in advance.

Key pool attributes include core size, maximum size, and queue capacity, which map directly to ThreadPoolExecutor fields.

When Does a Thread Pool Trigger a Rejection Policy?

Unlike data source pools, a thread pool also has a blocking queue. Rejection occurs only after the core pool is full, the queue is saturated, and the maximum pool size is reached. In other words, rejection is triggered when submittedTasks > (maxPoolSize + queueCapacity).

JDK Built‑in Rejection Policies

RejectedExecutionHandler Interface

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

CallerRunsPolicy (Caller‑Runs Policy)

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}

Function: The submitting thread runs the task if the pool is not shut down.

Use case: Suitable for low‑concurrency scenarios where task failure is unacceptable and performance requirements are modest.

AbortPolicy (Abort Policy)

public static class AbortPolicy implements RejectedExecutionHandler {
    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 JDK policy; callers must handle the exception.

Note: The default queues in ExecutorService are unbounded, so they rarely trigger rejection unless a custom bounded queue is used.

DiscardPolicy (Discard Policy)

public static class DiscardPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        // silently discard
    }
}

Function: Silently discards the task.

Use case: When the task is non‑essential and can be dropped without impact.

DiscardOldestPolicy (Discard‑Oldest Policy)

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll();
            e.execute(r);
        }
    }
}

Function: Removes the oldest queued task and attempts to execute the new one.

Use case: Useful when newer tasks have higher priority than older pending ones.

Third‑Party Rejection Policy Implementations

Dubbo – AbortPolicyWithReport

public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
    private final String threadName;
    private final URL url;
    // ... other fields omitted
    @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() { /* implementation omitted */ }
}

Dubbo logs detailed pool parameters, dumps the thread stack, and then throws the exception.

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 new thread for the rejected task, suitable for high‑performance scenarios.

ActiveMQ – Custom Handler

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 tries to re‑queue the task for up to one minute before giving up.

Pinpoint – 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 rejection occurs.

Conclusion

The article introduced thread‑pool design concepts, explained when ThreadPoolExecutor triggers rejection, presented the four JDK built‑in policies, and described four third‑party implementations, providing a comprehensive view of possible strategies and their appropriate 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.

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