Understanding Java ThreadPool Rejection Policies and Their Use Cases
This article explains the conditions that trigger Java thread‑pool rejection policies, describes the four built‑in policies (AbortPolicy, CallerRunsPolicy, DiscardPolicy, DiscardOldestPolicy), provides detailed code examples for each, and discusses suitable application scenarios for backend developers.
Trigger Conditions for Rejection Policies
The rejection policy is activated when the number of submitted tasks exceeds maxPoolSize + queueCapacity . When tasks exceed the core pool size, they are first placed in the blocking queue; if the queue is full, new threads are created up to maxPoolSize . If the pool cannot grow further, the configured rejection handler is invoked.
JDK Built‑in RejectionPolicy Interface
JDK defines the RejectedExecutionHandler interface. Its rejectedExecution(Runnable r, ThreadPoolExecutor executor) method receives the rejected task and the executor instance, allowing custom handling. The JDK provides four standard implementations: AbortPolicy , CallerRunsPolicy , DiscardPolicy , and DiscardOldestPolicy .
Four Built‑in Rejection Policies
1. AbortPolicy (Default)
Throws a RejectedExecutionException when a task cannot be accepted. Suitable for critical business logic where task loss must be detected immediately; the exception must be caught to avoid program termination.
2. CallerRunsPolicy
If the pool is not shut down, the thread that submitted the task runs it itself. This avoids task loss and is appropriate for low‑throughput, non‑time‑critical workloads, though it may increase the caller’s load.
3. DiscardPolicy
Silently discards the rejected task without any exception. Useful when the task is non‑essential (e.g., logging or statistics) and losing it does not affect system correctness.
4. DiscardOldestPolicy
Removes the oldest task in the queue and attempts to enqueue the new one. Ideal when newer tasks have higher priority, such as real‑time trading requests, but it can cause data loss if not used carefully.
Code Examples and Application Scenarios
AbortPolicy Example
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadPoolExecutor.AbortPolicy;
import java.util.concurrent.RejectedExecutionException;
public class AbortPolicyDemo {
private static final int THREADS_SIZE = 1;
private static final int CAPACITY = 1;
public static void main(String[] args) throws Exception {
ThreadPoolExecutor pool = new ThreadPoolExecutor(THREADS_SIZE, THREADS_SIZE, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue
(CAPACITY));
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 0; i < 10; i++) {
Runnable myrun = new MyRunnable("task-" + i);
pool.execute(myrun);
}
} catch (RejectedExecutionException e) {
e.printStackTrace();
pool.shutdown();
}
}
static class MyRunnable implements Runnable {
private String name;
MyRunnable(String name) { this.name = name; }
@Override
public void run() {
try {
System.out.println(name + " is running.");
Thread.sleep(200);
} catch (Exception e) { e.printStackTrace(); }
}
}
}In high‑value business scenarios (e.g., financial transaction processing), using AbortPolicy makes task failures visible immediately, allowing logging, alerts, or compensating actions.
CallerRunsPolicy Example
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CallerRunsPolicyDemo {
private static final int THREADS_SIZE = 1;
private static final int CAPACITY = 1;
public static void main(String[] args) throws Exception {
ThreadPoolExecutor pool = new ThreadPoolExecutor(THREADS_SIZE, THREADS_SIZE, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue
(CAPACITY));
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
Runnable myrun = new MyRunnable("task-" + i);
pool.execute(myrun);
}
pool.shutdown();
}
static class MyRunnable implements Runnable {
private String name;
MyRunnable(String name) { this.name = name; }
@Override
public void run() {
try {
System.out.println(name + Thread.currentThread() + " is running.");
Thread.sleep(1000);
} catch (Exception e) { e.printStackTrace(); }
}
}
}This policy fits small internal tools where tasks must not be dropped and occasional extra load on the submitting thread is acceptable.
DiscardPolicy Example
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class DiscardPolicyDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue
(2));
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Executing task " + taskId + " by " + Thread.currentThread().getName());
try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
}
executor.shutdown();
}
}Appropriate for low‑priority jobs such as simple logging where occasional loss is tolerable.
DiscardOldestPolicy Example
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy;
import java.util.concurrent.TimeUnit;
public class DiscardOldestPolicyDemo {
private static final int THREADS_SIZE = 1;
private static final int CAPACITY = 1;
public static void main(String[] args) throws Exception {
ThreadPoolExecutor pool = new ThreadPoolExecutor(THREADS_SIZE, THREADS_SIZE, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue
(CAPACITY));
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
for (int i = 0; i < 10; i++) {
pool.execute(new MyRunnable("task-" + i));
}
}
}Used when newer tasks have higher priority (e.g., real‑time trading), but developers must accept possible loss of older queued work.
Conclusion
AbortPolicy : default, throws exception, best for critical tasks that require immediate failure detection.
CallerRunsPolicy : runs rejected task in the caller thread, avoids loss, suitable for low‑load, non‑time‑critical scenarios.
DiscardPolicy : silently drops tasks, ideal for non‑essential work such as logging.
DiscardOldestPolicy : discards the oldest queued task to make room for a new one, useful when new tasks are more important.
Top Architecture Tech Stack
Sharing Java and Python tech insights, with occasional practical development tool tips.
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.