Thread Pools & ExecutorService
Design a pool as bounded workers + bounded queue + explicit overload policy.
📌 Key IdeaA thread pool is a concurrency control system, not just a performance trick. The core design is worker lifecycle, queue behavior, rejection strategy, and shutdown semantics.
ThreadPoolExecutor gives these knobs in production.Problem it solves
- Creating one thread per request can exhaust memory and scheduler capacity under load.
- Without bounded queues/pools, traffic spikes cause latency blowups and cascading failures.
- Thread pools provide controlled concurrency, backpressure, and predictable overload behavior.
Why It Matters
- Without bounds, task bursts turn into memory growth, high latency, and thread thrashing.
- Interviewers expect you to explain overload behavior, not only
Executors.newFixedThreadPool(). - Most outages here are policy mistakes: unbounded queue, wrong rejection policy, or missing shutdown.
Visual
🧵 Thread pool design from scratch
submit(task) | +--> [state check: RUNNING?] | +--> [try spawn worker until coreSize] | | | +--> worker loop: take() -> run() -> repeat | +--> [else enqueue in bounded queue] | | | +--> if queue full: rejection policy | shutdown() -> stop accept -> drain/finish -> terminate
When to Use
- Server workloads executing many independent short tasks.
- You need predictable throughput and backpressure under burst traffic.
- You want to integrate with Future/CompletableFuture and observability metrics.
When NOT to Use
- Very long/blocking operations mixed with latency-sensitive tasks in the same pool.
- CPU-bound jobs with oversized pools (context-switch overhead).
- Workloads requiring per-task fairness/priority without queue design.
Code Variations
1️⃣ Minimal custom thread pool (from scratch)java
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
final class MiniThreadPool {
private final BlockingQueue<Runnable> queue;
private final List<Thread> workers = new ArrayList<>();
private final AtomicBoolean running = new AtomicBoolean(true);
MiniThreadPool(int nWorkers, int queueCapacity) {
this.queue = new ArrayBlockingQueue<>(queueCapacity);
for (int i = 0; i < nWorkers; i++) {
Thread t = new Thread(this::workerLoop, "mini-pool-" + i);
t.start();
workers.add(t);
}
}
void execute(Runnable task) {
if (!running.get()) throw new RejectedExecutionException("pool stopped");
if (!queue.offer(task)) {
// simple rejection policy: fail fast
throw new RejectedExecutionException("queue full");
}
}
private void workerLoop() {
while (running.get() || !queue.isEmpty()) {
try {
Runnable task = queue.poll(200, TimeUnit.MILLISECONDS);
if (task != null) task.run();
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
} catch (Throwable t) {
// log and continue; one bad task should not kill worker loop
}
}
}
void shutdown() {
running.set(false);
for (Thread t : workers) t.interrupt();
}
}Interview narrative: start with this skeleton, then discuss task submission race, richer rejection policies, and graceful termination.
2️⃣ Production-grade equivalent using ThreadPoolExecutorjava
ThreadPoolExecutor pool = new ThreadPoolExecutor(
8, // corePoolSize
16, // maxPoolSize (burst)
60, TimeUnit.SECONDS, // keepAlive for extra threads
new ArrayBlockingQueue<>(500),
r -> {
Thread t = new Thread(r);
t.setName("orders-pool-" + t.threadId());
return t;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
pool.prestartAllCoreThreads();CallerRunsPolicy pushes back on producers during overload.3️⃣ Correct shutdown lifecyclejava
pool.shutdown(); // stop accepting new tasks
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
List<Runnable> dropped = pool.shutdownNow(); // emergency fallback
// persist/requeue dropped tasks if needed
}Always define behavior for in-flight and not-yet-started tasks.
Design Decisions You Must Make
| Aspect | Option A | Option B |
|---|---|---|
| Queue type | Unbounded queue (simple, risky memory) | Bounded queue (safe memory, needs rejection policy) |
| Worker count | Fixed pool (predictable) | Core + burst max (adaptive, tune carefully) |
| Overload handling | Drop/reject fast | CallerRuns/slow producer (backpressure) |
| Shutdown | Hard stop (shutdownNow) for emergencies | Graceful (shutdown + awaitTermination) |
Common Confusions Cleared Up
Queue-first vs thread-first behavior
With a bounded queue, tasks fill queue before spawning beyond core threads (depending on config). This affects latency and memory profile under bursts.
Rejection policy is part of API contract
AbortPolicy throws; CallerRuns slows caller; Discard/DiscardOldest lose work. Choose intentionally per business criticality.
One pool per workload class
Separate pools for CPU-heavy, blocking I/O, and scheduled jobs. Mixing everything in one pool causes head-of-line blocking.
Interview Q&A
Quick Cheat Sheet
Design coreWorkers + bounded queue + state + rejection.
Golden ruleBound both threads and queue.
Overload strategyRejection policy is mandatory design choice.
Lifecycleshutdown + awaitTermination + fallback shutdownNow.
MetricsQueue depth, active threads, task latency, rejection count.