What types of Thread Pool exist in Java?
Java provides several ready-made thread pool types through the Executors factory. Each type is optimized for a specific scenario.
Junior Level
Basic Understanding
Java provides several ready-made thread pool types through the Executors factory. Each type is optimized for a specific scenario.
1. FixedThreadPool
Fixed number of threads. All tasks execute in parallel, the rest wait in a queue. Choose when you know the maximum load in advance (e.g., processing 10 files simultaneously).
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
pool.submit(() -> {
// Maximum 10 tasks execute simultaneously
// The remaining 90 wait in the queue
});
}
| Parameter | Value |
|---|---|
| Pool size | Fixed (10) |
| Queue | Unbounded (LinkedBlockingQueue) |
| When to use | Known, stable load |
2. CachedThreadPool
Creates new threads as needed, reuses free ones.
ExecutorService pool = Executors.newCachedThreadPool();
pool.submit(() -> {
// If no free threads — a new one is created
// If a thread is free for 60 seconds — it's destroyed
});
| Parameter | Value |
|---|---|
| Pool size | 0 to Integer.MAX_VALUE (theoretically ~2 billion, practically limited by OS memory — usually 32K threads max on Linux) |
| Queue | SynchronousQueue (capacity 0) |
| When to use | Many short tasks |
3. SingleThreadExecutor
Exactly one thread. Tasks execute strictly sequentially.
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.submit(() -> System.out.println("Task 1"));
pool.submit(() -> System.out.println("Task 2"));
// Always: "Task 1" → "Task 2"
| Parameter | Value |
|---|---|
| Pool size | 1 |
| Queue | Unbounded |
| When to use | Sequential execution |
4. ScheduledThreadPool
Scheduling tasks with a delay or periodically.
ScheduledExecutorService pool = Executors.newScheduledThreadPool(5);
// Execute after 10 seconds
pool.schedule(() -> System.out.println("Delayed"), 10, TimeUnit.SECONDS);
// Execute every 5 seconds
pool.scheduleAtFixedRate(() -> System.out.println("Periodic"), 0, 5, TimeUnit.SECONDS);
| Parameter | Value |
|---|---|
| Pool size | Fixed |
| Queue | DelayedWorkQueue |
| When to use | Periodic tasks |
5. WorkStealingPool (Java 8+)
Note: in Java 21+ with virtual threads (Project Loom), for I/O-bound tasks prefer
Executors.newVirtualThreadPerTaskExecutor(). Virtual threads cost ~KB instead of ~1MB and can be created by the millions.
Uses ForkJoinPool with work-stealing algorithm.
ExecutorService pool = Executors.newWorkStealingPool();
// By default: number of threads = number of CPU cores
| Parameter | Value |
|---|---|
| Pool size | Number of CPU cores |
| Algorithm | Work-stealing |
| When to use | Parallelizing computations |
Comparison Table
| Type | Size | Queue | Best for |
|---|---|---|---|
| FixedThreadPool | Fixed | Unbounded | Stable load |
| CachedThreadPool | Dynamic | SynchronousQueue | Short tasks |
| SingleThreadExecutor | 1 | Unbounded | Sequential execution |
| ScheduledThreadPool | Fixed | DelayedWorkQueue | Scheduling |
| WorkStealingPool | CPU cores | Work-stealing | Computations |
Middle Level
FixedThreadPool — Details
// Internal implementation
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads, // corePoolSize
nThreads, // maximumPoolSize
0L, TimeUnit.MILLISECONDS, // keepAliveTime
new LinkedBlockingQueue<Runnable>() // UNBOUNDED queue
);
}
Danger: If tasks arrive faster than they’re processed, the queue grows infinitely → OutOfMemoryError.
Solution: Create manually with a bounded queue:
ExecutorService safePool = new ThreadPoolExecutor(
10, 10, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1000) // Limit 1000 tasks
);
CachedThreadPool — Details
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0, // corePoolSize
Integer.MAX_VALUE, // maximumPoolSize — no limit!
60L, TimeUnit.SECONDS, // Threads die after 60s
new SynchronousQueue<Runnable>() // Capacity = 0
);
}
Danger: Under a load spike, it may create thousands of threads → OutOfMemoryError: unable to create new native thread.
SingleThreadExecutor — Details
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()
)
);
}
Difference from newFixedThreadPool(1): guarantees that settings cannot be changed (encapsulation).
ScheduledThreadPool — Details
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);
// One-time execution after a delay
scheduler.schedule(task, 10, TimeUnit.SECONDS);
// Fixed rate (from the start of the previous run)
scheduler.scheduleAtFixedRate(task, 0, 5, TimeUnit.SECONDS);
// Fixed delay (from the end of the previous run)
scheduler.scheduleWithFixedDelay(task, 0, 5, TimeUnit.SECONDS);
// scheduleAtFixedRate: if task takes 7 seconds, and period is 5s —
// the next one launches IMMEDIATELY after completion (skip)
// scheduleWithFixedDelay: waits 5 seconds AFTER the previous completion
WorkStealingPool — Details
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool(
Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, // UncaughtExceptionHandler
true // asyncMode
);
}
Work-stealing algorithm:
- Each thread has its own task queue
- If a thread finishes its tasks — it “steals” from the tail of another thread’s queue
- Great for uneven tasks, because a free thread doesn’t idle but “steals” unprocessed tasks from busy threads (work-stealing algorithm).
Which pool NOT to use
- NOT CachedThreadPool for long tasks → thread explosion (thousands of threads)
- NOT FixedThreadPool with LinkedBlockingQueue for unpredictable load → OOM (queue grows infinitely)
- NOT SingleThreadExecutor for CPU-bound parallelism → one thread, no parallelism
- NOT WorkStealingPool for I/O-bound → blocking a thread blocks work-stealing
Senior Level
When Executors is Not Suitable
Senior developers often avoid Executors.* methods and create ThreadPoolExecutor directly.
Why:
| Problem | Executors.* | ThreadPoolExecutor manually |
|---|---|---|
| Queue limit | No (OOM risk) | Yes (ArrayBlockingQueue) |
| RejectedExecutionHandler | AbortPolicy | Any (CallerRunsPolicy) |
| ThreadFactory | Default | Custom (names, priorities) |
| keepAliveTime | Fixed | Configurable |
Choosing a Pool Based on Task Type
| Task type | Recommended pool | Formula |
|---|---|---|
| CPU-bound (computations) | FixedThreadPool | N or N+1 cores |
| I/O-bound (DB, API) | FixedThreadPool or custom | N * (1 + W/S) |
| Short tasks | CachedThreadPool (with limit!) | maxPoolSize limited |
| Recursion / Streams | ForkJoinPool | N cores |
| Background tasks | ScheduledThreadPool | Depends on frequency |
Sizing Formula for Mixed Loads
Threads = Number of Cores * Target CPU Utilization * (1 + Wait time / Service time)
Example:
- 8-core CPU
- Target utilization: 80% (0.8)
- 90% of time waiting (W/S = 9)
Threads = 8 * 0.8 * (1 + 9) = 8 * 0.8 * 10 = 64 threads
Thread Starvation
I/O-bound pool too small (5 threads):
→ All 5 threads waiting for DB response
→ New requests waiting in queue
→ System "stalled"
Solution: increase pool size for I/O-bound tasks
Sizing Formula in Code
public class PoolSizer {
public static int calculatePoolSize(int cores, double targetUtilization,
double waitTime, double serviceTime) {
return (int) Math.ceil(
cores * targetUtilization * (1 + waitTime / serviceTime)
);
}
public static void main(String[] args) {
int cores = Runtime.getRuntime().availableProcessors();
int poolSize = calculatePoolSize(cores, 0.8, 90, 10);
// 8 * 0.8 * (1 + 90/10) = 64
System.out.println("Recommended pool size: " + poolSize);
}
}
Diagnostics
// Pool monitoring
ThreadPoolExecutor executor = ...;
// Critical metrics
executor.getActiveCount(); // How many threads working
executor.getQueue().size(); // Growing queue = problem
executor.getCompletedTaskCount(); // Progress
executor.getPoolSize(); // Current size
Best Practices
- Avoid Executors.newCachedThreadPool() in production — use custom with limit
- Limit the queue — ArrayBlockingQueue instead of LinkedBlockingQueue
- Sizing formula — CPU-bound: N+1, I/O-bound: N * (1 + W/S)
- WorkStealingPool — for recursive tasks and parallel streams
- ScheduledThreadPool — for periodic tasks (cron-like)
- Monitor queue.size() — growing queue = degradation
- Custom ThreadFactory — name threads for debugging
Interview Cheat Sheet
Must know:
- 5 pool types: FixedThreadPool (fixed size), CachedThreadPool (dynamic), SingleThreadExecutor (1 thread), ScheduledThreadPool (scheduled), WorkStealingPool (work-stealing algorithm)
- FixedThreadPool uses LinkedBlockingQueue without a limit — OOM risk under unpredictable load
- CachedThreadPool: maximumPoolSize = Integer.MAX_VALUE, SynchronousQueue (capacity 0) — thread explosion risk
- WorkStealingPool = ForkJoinPool with asyncMode=true, each thread has its own deque queue
- scheduleAtFixedRate — from start of previous run (may skip if task is long), scheduleWithFixedDelay — from end
- SingleThreadExecutor guarantees encapsulation — settings cannot be changed, unlike newFixedThreadPool(1)
- In production, avoid Executors.* — create ThreadPoolExecutor manually with bounded queue and RejectedExecutionHandler
- Sizing formula: CPU-bound = N+1, I/O-bound = N * (1 + W/S), mixed = N * util * (1 + W/S)
Frequent follow-up questions:
- How does FixedThreadPool differ from SingleThreadExecutor? — FixedThreadPool can be reconfigured, SingleThreadExecutor is encapsulated (FinalizableDelegatedExecutorService)
- Why is CachedThreadPool dangerous in production? — Under a load spike, creates thousands of threads → unable to create new native thread
- How does work-stealing work? — Each thread has its own deque; a free thread “steals” tasks from the tail of another’s deque (FIFO), the owner takes from the head (LIFO)
- What to choose for I/O-bound tasks? — FixedThreadPool with formula N * (1 + W/S) or virtual threads (Java 21+)
Red flags (do NOT say):
- “CachedThreadPool is my production choice” — dangerous without limits, need maxPoolSize
- “FixedThreadPool with LinkedBlockingQueue is safe” — unbounded queue = OOM
- “WorkStealingPool is good for I/O” — no, blocking a thread blocks work-stealing
- “scheduleAtFixedRate guarantees the interval” — no, if the task is longer than the period — the next runs immediately
Related topics:
- [[12. What is Thread Pool]]
- [[15. What does ExecutorService do]]
- [[16. What is the difference between Executors.newFixedThreadPool() and newCachedThreadPool()]]
- [[17. What is ForkJoinPool]]