ThreadPoolExecutor.CallerRunsPolicy executes a rejected task on the thread calling execute while the executor is running.
Java CallerRunsPolicy: a full worker queue moves work onto the submitter
Make saturation observable
The fixture holds one worker in a latch and fills its single queued slot. A third execute call then runs on main. This slows submission, but it also moves arbitrary task work into the caller's latency budget. An HTTP request thread could spend its deadline processing background work if this policy were copied into a server without thought.
The policy discards a rejected task if the executor is already shut down. That is a poor default where every accepted order must receive a durable outcome. Explicit rejection handling is the safer contract for those workflows.
Size both queues
A fixed worker count does not cap retained work if its queue is unbounded. Here both worker count and queue size are one, so the third submission must meet the policy. The latch makes this deterministic; a sleep-based demo would depend on machine speed.
The result proves where the third task ran, not that caller execution is always suitable. Blocking tasks, thread-local request state, and nested submissions can create latency or deadlock paths. Thread-local cleanup matters when code runs on a different thread than expected.
Working program
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class SaturatedDispatchPool {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor workers = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.CallerRunsPolicy());
CountDownLatch firstStarted = new CountDownLatch(1);
CountDownLatch releaseFirst = new CountDownLatch(1);
CountDownLatch queuedFinished = new CountDownLatch(1);
AtomicReference<Thread> overflowThread = new AtomicReference<>();
try {
workers.execute(() -> {
firstStarted.countDown();
try { releaseFirst.await(); }
catch (InterruptedException stopped) { Thread.currentThread().interrupt(); }
});
if (!firstStarted.await(2, TimeUnit.SECONDS)) throw new IllegalStateException("start deadline");
workers.execute(queuedFinished::countDown);
workers.execute(() -> overflowThread.set(Thread.currentThread()));
System.out.println(overflowThread.get() == Thread.currentThread());
releaseFirst.countDown();
System.out.println(queuedFinished.await(2, TimeUnit.SECONDS));
} finally {
releaseFirst.countDown();
workers.shutdown();
if (!workers.awaitTermination(2, TimeUnit.SECONDS)) workers.shutdownNow();
}
}
}Output
true
trueCost and ownership
The executor holds one active and one queued task in this fixture; overflow work uses the caller's time and stack. CallerRunsPolicy is feedback through latency, not a persisted admission record. If the caller abandons the request, the application still needs an outcome policy for its work.
Common Mistakes
- Do not assume a fixed worker count also bounds an unbounded task queue.
- Do not use CallerRunsPolicy where submitter latency is hard-bounded without accounting for task duration.
- Do not assume shutdown submissions are executed; this policy discards them.
Read next
Java bounded executors: test saturation and rejected work, Java ExecutorService: bounded admission and shutdown, Java ExecutorCompletionService: consume finished tasks, not submission order, Java ThreadLocal: clear request state on reused worker threads.
