A ThreadPoolExecutor with a bounded queue can reject a submitted task when its workers and queue cannot accept more work.
Java bounded executors: test saturation and rejected work
The complete program targets Java 8. Compile it as one source file; its output is checked against the lesson.
Backlog needs a limit
An importer with one worker and one queue slot can hold one running task and one waiting task. A third submission must meet an explicit rejection rule. An unbounded queue changes the failure mode to growing memory and delay; it does not create extra processing capacity.
The fixture blocks the worker on a latch, fills the queue, and checks AbortPolicy rejection. It then releases the worker in finally. That arrangement controls saturation without relying on sleeps or guessing how quickly a worker starts.
Choose what the caller observes
CallerRunsPolicy moves execution onto the submitting thread, which can slow admission but may also block an HTTP request or event-loop thread. Silently discarding a task can lose work while the caller believes it was accepted. Neither policy is universally correct.
A queue limit does not supply durable storage. If the JVM exits, queued work disappears. A service that promises eventual processing needs persistence and replay semantics outside this local executor demonstration.
Working program
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ImportAdmission {
public static void main(String[] args) throws Exception {
CountDownLatch release = new CountDownLatch(1);
ThreadPoolExecutor worker = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(1), new ThreadPoolExecutor.AbortPolicy());
try {
worker.execute(() -> { try { release.await(); } catch (InterruptedException stop) { Thread.currentThread().interrupt(); } });
worker.execute(() -> {});
try { worker.execute(() -> {}); }
catch (RejectedExecutionException rejected) { System.out.println("capacity rejected"); }
System.out.println("queued=" + worker.getQueue().size());
} finally { release.countDown(); worker.shutdown(); }
if (!worker.awaitTermination(5, TimeUnit.SECONDS)) throw new AssertionError("worker did not stop");
}
}Output
capacity rejected
queued=1Costs and boundaries
This queue holds at most one task and the pool has one worker. Retained task closures can still reference large payloads, so a bounded task count is not an exact byte budget. This test checks admission and termination, not production throughput.
Common Mistakes
- Do not hide rejected work behind a success response.
- Bound payload size as well as queue length.
- Release test latches even when an assertion fails.
Read next
Java ExecutorService: bounded admission and shutdown, Java blocking queues: bounded capacity and backpressure, Async boundaries.
Compare the Python boundary
Python thread pools: collect results and observe worker failures.
