Definition
- corePoolSize – the number of worker threads that are always kept alive, even when idle.
- maximumPoolSize – the maximum number of workers under burst load. Non-core workers are created only when the queue is full.
- keepAliveTime + timeUnit – how long idle non-core workers wait before being terminated.
- workQueue – a
BlockingQueuewhere tasks wait when all core workers are busy. MUST be bounded in production. - threadFactory – creates worker threads. Use it to set thread names, daemon status, and priority for debugging.
- rejectedExecutionHandler – the policy when the queue is full AND maximum workers are reached. Four built-in options plus custom.
- Task flow – submit(task) → workers < core? Create worker. Workers >= core? Queue task. Queue full? Workers < max? Create non-core worker. Workers = max AND queue full? Rejection handler fires.
At National Bank of India: core workers (5) are permanent tellers always on duty. Max workers (10) means 5 extra tellers can be called during rush hour. The queue (100 seats) is the waiting area. keepAlive (60 seconds) means extra tellers go home after 60 seconds of no customers. threadFactory is HR's hiring policy (name badges, department). When the waiting area is full and all 10 tellers are busy: AbortPolicy says "Sorry, come back later!" CallerRunsPolicy means the manager serves the customer personally.
Understanding the Concepts
What are the 7 parameters of ThreadPoolExecutor?
package com.bank.concurrency;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPoolConfigDemo {
private static final AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60, TimeUnit.SECONDS, // keepAliveTime + unit
new LinkedBlockingQueue<>(100), // workQueue (BOUNDED!)
r -> { // threadFactory
Thread t = new Thread(r,
"payment-worker-" + counter.incrementAndGet());
t.setDaemon(false);
return t;
},
new ThreadPoolExecutor.CallerRunsPolicy() // rejectionHandler
);
}
}
How does the task submission flow work?
When a task is submitted, the pool follows a precise decision tree.
- If the number of running workers is less than corePoolSize, create a new core worker to run the task immediately.
- If core workers are all busy, offer the task to the workQueue.
- If the queue accepts the task, it waits for a free worker.
- If the queue is full (bounded), and workers are less than maximumPoolSize, create a non-core worker.
- If workers equal maximumPoolSize AND the queue is full, the rejection handler fires.
What are the four rejection policies?
// 1. AbortPolicy (DEFAULT): throws RejectedExecutionException
new ThreadPoolExecutor.AbortPolicy();
// Use when task loss is unacceptable. Caller must catch and handle.
// 2. CallerRunsPolicy: the submitting thread executes the task
new ThreadPoolExecutor.CallerRunsPolicy();
// BEST for production: creates natural back-pressure
// The submitter is busy executing, so it cannot submit more tasks
// 3. DiscardPolicy: silently drops the task
new ThreadPoolExecutor.DiscardPolicy();
// DANGEROUS: no exception, no log. Silent data loss.
// 4. DiscardOldestPolicy: drops the oldest task in the queue, retries new one
new ThreadPoolExecutor.DiscardOldestPolicy();
// Use when latest data is more important than old (real-time dashboards)
What is a custom rejection handler?
package com.bank.concurrency;
import java.util.concurrent.*;
public class CustomHandlerDemo {
static final RejectedExecutionHandler handler = (runnable, executor) -> {
// 1. Metrics
System.out.println("Task rejected! Queue: " + executor.getQueue().size()
+ "/100, Active: " + executor.getActiveCount()
+ "/" + executor.getMaximumPoolSize());
// 2. Fallback: CallerRuns as back-pressure
if (!executor.isShutdown()) {
runnable.run();
}
};
}
What are common mistakes?
Using an unbounded queue hides pool saturation. Tasks pile up until the heap is exhausted.
// WRONG: unbounded queue (OOM risk)
new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>()); // capacity = Integer.MAX_VALUE!
// CORRECT: bounded queue with rejection policy
new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500),
new ThreadPoolExecutor.CallerRunsPolicy());
What are common interview follow-up questions?
- Why does the pool create non-core workers only when the queue is full? To avoid creating expensive threads when the queue can absorb the burst.
- Can corePoolSize be zero? Yes. With core=0 and a SynchronousQueue, this is how newCachedThreadPool() works: every task either reuses an idle thread or creates a new one.
Visual Overview
ThreadPoolExecutor Task Flow
════════════════════════════
submit(task)
│
▼
workers < core? ──YES──→ Create core worker, run task
│ NO
▼
queue.offer(task)? ──YES──→ Task queued, waits for worker
│ NO (queue full)
▼
workers < max? ──YES──→ Create non-core worker, run task
│ NO (max reached)
▼
REJECTION HANDLER fires
┌─────────────────────────────────────┐
│ AbortPolicy: throw exception │
│ CallerRunsPolicy: caller executes │
│ DiscardPolicy: silent drop │
│ DiscardOldestPolicy: drop oldest │
└─────────────────────────────────────┘
Key Takeaway
ThreadPoolExecutor is the most configurable thread pool in Java with 7 parameters. The task flow follows a clear sequence: create core workers first, then queue tasks, then create non-core workers on burst, then reject. Understanding this flow is essential for sizing pools correctly.
The four rejection policies handle overload differently. CallerRunsPolicy is the best default for production because it creates natural back-pressure: when the pool is saturated, the submitting thread executes the task itself, which slows down submission and prevents queue growth. DiscardPolicy is dangerous because it silently loses tasks with no exception or log entry.
Always use bounded queues in production. An unbounded queue hides pool saturation and lets tasks pile up until the heap is exhausted. Always use a custom ThreadFactory with named threads so thread dumps are readable.
At National Bank of India, a payment processing pool with CallerRunsPolicy survived a gateway outage: when the queue filled up, Tomcat threads executed payments themselves, increasing request latency but preventing OOM crashes.
Remember one sentence: ThreadPoolExecutor follows the flow core-then-queue-then-max-then-reject, and always use bounded queues with CallerRunsPolicy for production safety.
---