What is the difference between Executors.newFixedThreadPool() and newCachedThreadPool()?
Both pools create an ExecutorService, but their behavior differs radically.
Junior Level
Basic Understanding
Both pools create an ExecutorService, but their behavior differs radically.
newFixedThreadPool
ExecutorService pool = Executors.newFixedThreadPool(10);
- Fixed number of threads (10)
- Tasks wait in a queue if all threads are busy
- Threads never die — always ready to work
newCachedThreadPool
ExecutorService pool = Executors.newCachedThreadPool();
- Dynamic number of threads (0 to infinity)
- Each task immediately gets a thread (new or free)
- Free threads die after 60 seconds
Comparison
| Characteristic | FixedThreadPool | CachedThreadPool |
|---|---|---|
| Pool Size | Fixed | Dynamic |
| Queue | LinkedBlockingQueue (unbounded) | SynchronousQueue (capacity 0) |
| Idle thread | Lives forever | Dies after 60s |
| Best for | Even/steady load | Bursty load |
Middle Level
Configuration Under the Hood
newFixedThreadPool
new ThreadPoolExecutor(
n, // corePoolSize = n
n, // maximumPoolSize = n
0L, // keepAliveTime = 0
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>() // UNBOUNDED queue
);
newCachedThreadPool
new ThreadPoolExecutor(
0, // corePoolSize = 0
Integer.MAX_VALUE, // maximumPoolSize = infinity!
60L, // keepAliveTime = 60 seconds
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>() // Capacity = 0
);
How Tasks Are Passed
FixedThreadPool: Passive Queue
Task → Queue (LinkedBlockingQueue) → Free thread picks it up
[1] [2] [3] [4] [5] ... [1000] ... [∞]
All tasks accumulate in the queue. Threads process one at a time.
CachedThreadPool: Hand-to-Hand Transfer
Task → SynchronousQueue → IMMEDIATELY to a free thread
→ Or a new thread is created
SynchronousQueue has capacity 0. Each task MUST be handed to a thread immediately.
When to Use What
| Scenario | FixedThreadPool | CachedThreadPool |
|---|---|---|
| Web server (constant traffic) | Yes | No |
| Batch processing | Yes | No |
| Short HTTP requests | No | Yes |
| Background tasks (infrequent) | No | Yes |
Senior Level
Dangers of FixedThreadPool
Problem: Unbounded queue → OutOfMemoryError
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10_000_000; i++) {
pool.submit(() -> {
Thread.sleep(1000); // Slow task
});
}
// Queue: 10,000,000 Runnable objects → OutOfMemoryError!
Solution: Bounded queue
ExecutorService safePool = new ThreadPoolExecutor(
10, 10, 0L, MILLISECONDS,
new ArrayBlockingQueue<>(10000), // Limit!
new ThreadPoolExecutor.CallerRunsPolicy() // Back-pressure
);
Dangers of CachedThreadPool
Problem: Thread explosion → OutOfMemoryError: unable to create new native thread
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 100_000; i++) {
pool.submit(() -> {
Thread.sleep(5000); // Long task
});
}
// Will create 100,000 threads! OS can't handle → OOM
Solution: Limit maximumPoolSize
ExecutorService safeCached = new ThreadPoolExecutor(
0, 200, // Maximum 200 threads!
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
Mechanism Comparison (Advanced)
| Characteristic | FixedThreadPool | CachedThreadPool |
|---|---|---|
| Resource Limit | Hard limit on threads | None (risk of OOM from threads) |
| Queue | Passive (accumulating) | Active (hand-to-hand) |
| Thread idle | Threads never die | Threads removed after a minute |
| Latency | ~higher (task waits in queue for a free thread, +0-1000ms) | ~lower (immediately gets a thread, ~0ms) |
| Memory | Stable | Grows during burst |
Production Recommendations
In production, Executors.newCachedThreadPool() is dangerous without limits, because during load spikes it creates thousands of threads. For short tasks, use a custom ThreadPoolExecutor with maxPoolSize. In Java 21+, consider virtual threads.
Why:
- Violates the predictability principle
- During load spikes, creates thousands of threads
- Can kill the OS or JVM
How to do it right:
// Safe alternative to CachedThreadPool
ExecutorService safePool = new ThreadPoolExecutor(
0, // corePoolSize
200, // maximumPoolSize — hard limit!
60L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadPoolExecutor.CallerRunsPolicy() // Back-pressure
);
Diagnostics
VisualVM / JConsole
FixedThreadPool: ──────────────── stable line (10 threads)
CachedThreadPool: /\/\/\/\ "forest" — appearing and disappearing
Thread Dumps
jstack <pid>
CachedThreadPool problems:
"pool-1-thread-1001" #1010 RUNNABLE
"pool-1-thread-1002" #1011 RUNNABLE
...
// Hundreds of threads waiting on SynchronousQueue
Monitoring
// Threshold depends on task size in memory.
// If Runnable = 1KB, then 1000 tasks = 1MB.
// If Runnable captures large objects — threshold should be lower.
int queueSize = ((ThreadPoolExecutor)pool).getQueue().size();
if (queueSize > 1000) {
log.warn("Queue is growing! Possible OOM risk");
}
// CachedThreadPool — monitor pool size
int poolSize = ((ThreadPoolExecutor)pool).getPoolSize();
if (poolSize > 100) {
log.warn("Too many threads! Possible thread explosion");
}
Best Practices
Java 21+: For short tasks,
Executors.newVirtualThreadPerTaskExecutor()replaces CachedThreadPool. Virtual threads cost ~KB (vs ~1MB for regular threads) and can be created by the millions without thread explosion risk.
- FixedThreadPool — for stable load with a known maximum
- CachedThreadPool — ONLY for short tasks with limited maxPoolSize
- Always bound the queue — ArrayBlockingQueue with a limit
- Always bound maxPoolSize — even for CachedThreadPool
- CallerRunsPolicy — back-pressure under overload
- Monitor queue.size() for FixedThreadPool
- Monitor pool size for CachedThreadPool
- Avoid Executors.* in production — create ThreadPoolExecutor directly
Interview Cheat Sheet
Must know:
- FixedThreadPool: fixed size, unbounded LinkedBlockingQueue, threads never die — OOM risk from queue
- CachedThreadPool: 0..Integer.MAX_VALUE threads, SynchronousQueue (capacity 0), threads die after 60s — thread explosion risk
- SynchronousQueue has capacity 0 — each task MUST be handed to a thread immediately or a new one is created
- FixedThreadPool latency is higher (task waits in queue), CachedThreadPool latency is lower (~0ms delay)
- FixedThreadPool — for stable even load; CachedThreadPool — for short tasks with bursty traffic
- In production both are dangerous: FixedThreadPool → OOM from queue, CachedThreadPool → unable to create new native thread
- Solution: custom ThreadPoolExecutor with ArrayBlockingQueue and maxPoolSize + CallerRunsPolicy for back-pressure
- Java 21+: Executors.newVirtualThreadPerTaskExecutor() replaces CachedThreadPool (~KB per thread vs ~1MB)
Frequent follow-up questions:
- Why can FixedThreadPool cause OOM? — LinkedBlockingQueue has no limit — if tasks arrive faster than processing, the queue grows infinitely
- Why can CachedThreadPool create thousands of threads? — SynchronousQueue doesn’t buffer, each free slot → new thread, maximumPoolSize = Integer.MAX_VALUE
- What is back-pressure and how does CallerRunsPolicy work? — The task runs in the submitting thread, which can’t submit new ones — automatically slows the source
- How to make a safe alternative to CachedThreadPool? — ThreadPoolExecutor with maxPoolSize=200 and CallerRunsPolicy
Red flags (DO NOT say):
- “CachedThreadPool is my default for any task” — dangerous for long tasks, thread explosion
- “FixedThreadPool is completely safe” — no, unbounded queue = OOM
- “SynchronousQueue stores tasks in a queue” — no, capacity 0, only direct handoff
- “Threads in CachedThreadPool live forever” — no, they die after 60 seconds of idleness
Related topics:
- [[12. What is a Thread Pool]]
- [[13. What types of Thread Pool exist in Java]]
- [[15. What does ExecutorService do]]
- [[17. What is ForkJoinPool]]