Question 13 · Section 9

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.

Language versions: English Russian Ukrainian

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

  1. Avoid Executors.newCachedThreadPool() in production — use custom with limit
  2. Limit the queue — ArrayBlockingQueue instead of LinkedBlockingQueue
  3. Sizing formula — CPU-bound: N+1, I/O-bound: N * (1 + W/S)
  4. WorkStealingPool — for recursive tasks and parallel streams
  5. ScheduledThreadPool — for periodic tasks (cron-like)
  6. Monitor queue.size() — growing queue = degradation
  7. 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]]