Question 15 · Section 9

What is the difference between Executors.newFixedThreadPool() and newCachedThreadPool()?

Both pools create an ExecutorService, but their behavior differs radically.

Language versions: English Russian Ukrainian

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.

  1. FixedThreadPool — for stable load with a known maximum
  2. CachedThreadPool — ONLY for short tasks with limited maxPoolSize
  3. Always bound the queue — ArrayBlockingQueue with a limit
  4. Always bound maxPoolSize — even for CachedThreadPool
  5. CallerRunsPolicy — back-pressure under overload
  6. Monitor queue.size() for FixedThreadPool
  7. Monitor pool size for CachedThreadPool
  8. 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]]