Definition
- Fixed thread pool – constant core and max size, unbounded queue. Predictable resource use for steady workloads. Danger: unbounded queue can OOM.
- Cached thread pool – zero core, unlimited max, SynchronousQueue. Creates a thread per task if no idle thread is available. Good for short bursty tasks. Danger: slow tasks create unbounded threads.
- Scheduled thread pool – runs tasks after a delay or on a repeating schedule.
scheduleAtFixedRateanchors to a clock;scheduleWithFixedDelayadds a gap after each completion. - ForkJoinPool – work-stealing deques, sized to approximately the number of cores. Excellent for recursive divide-and-conquer (parallel array sum, merge sort). Poor for blocking IO because blocked workers cannot steal and cores sit idle.
- Common pool –
ForkJoinPool.commonPool()is shared JVM-wide by parallel streams and default CompletableFutures. Blocking IO on it stalls everything.
At National Bank of India: fixed pool = a branch with exactly 10 tellers hired for the year. Cached pool = a branch that hires a temp the instant a customer walks in and lets temps go after a minute idle. Scheduled pool = the clerk who runs the interest posting job every night at 2 AM. ForkJoinPool = a team that splits a huge counting job into halves, halves of halves, and whoever finishes early grabs work from a busy colleague.
Understanding the Concepts
When do you use each executor?
Java provides four ready-made executor shapes, each designed for a specific workload pattern.
package com.bank.concurrency;
import java.util.concurrent.*;
public class ExecutorChoiceDemo {
public static void main(String[] args) {
// Fixed: steady stream of trade settlements
ExecutorService fixed = Executors.newFixedThreadPool(16);
// Cached: burst of SMS notifications
ExecutorService cached = Executors.newCachedThreadPool();
// Scheduled: daily interest posting at 2 AM
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
scheduled.scheduleAtFixedRate(
() -> System.out.println("Posting interest"),
0, 24, TimeUnit.HOURS);
// ForkJoinPool: sum a million transaction amounts
ForkJoinPool fjp = new ForkJoinPool();
}
}
Why is ForkJoinPool poor for blocking IO?
ForkJoinPool is sized to roughly one thread per core, assuming threads are always running. If a worker blocks on a database or HTTP call, that core sits idle. Other workers cannot steal from the blocked worker because its deque has no tasks to steal. The pool does not automatically grow to compensate (unless ManagedBlocker is used, which is unreliable for bulk IO).
How do the four executors compare?
| Executor | Best for | Danger | Production advice |
|---|---|---|---|
| Fixed | Steady bounded load | Unbounded queue can OOM | Use ThreadPoolExecutor with bounded queue |
| Cached | Short bursty tasks | Unbounded threads for slow tasks | Never for blocking/slow tasks |
| Scheduled | Timed/periodic jobs | Unbounded queue | Fine for most scheduling uses |
| ForkJoinPool | CPU-bound recursion | IO blocks all workers | Never for blocking IO |
Why are Executors factory methods dangerous in production?
The convenience factory methods hide dangerous defaults. newFixedThreadPool uses an unbounded LinkedBlockingQueue that can grow until OutOfMemoryError. newCachedThreadPool creates unlimited threads for slow tasks.
package com.bank.concurrency;
import java.util.concurrent.*;
public class SafePoolCreation {
// WRONG: Executors.newFixedThreadPool(16) uses unbounded queue
// CORRECT: explicit bounded queue and rejection policy
private static final ThreadPoolExecutor pool = new ThreadPoolExecutor(
16, 16, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(500), // bounded queue
new ThreadPoolExecutor.CallerRunsPolicy() // backpressure
);
}
What is the difference between scheduleAtFixedRate and scheduleWithFixedDelay?
scheduleAtFixedRate: runs every N time units measured from the start of each run. If a run takes longer than N, the next run starts immediately after (runs can bunch up but never overlap). scheduleWithFixedDelay: waits N time units after each run completes before starting the next. Runs never overlap and have a guaranteed gap between them.
package com.bank.scheduling;
import java.util.concurrent.*;
public class ScheduleDemo {
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// Fixed rate: run every 10 seconds regardless of execution time
// If the task takes 3s, next run starts at 10s, 20s, 30s...
// If the task takes 12s, next run starts immediately after (at 12s, 22s...)
scheduler.scheduleAtFixedRate(() -> postInterest(), 0, 10, TimeUnit.SECONDS);
// Fixed delay: wait 10 seconds after each completion
// If the task takes 3s, next run starts at 13s, 26s, 39s...
// Guaranteed 10s gap between completion and next start
scheduler.scheduleWithFixedDelay(() -> reconcile(), 0, 10, TimeUnit.SECONDS);
}
}
What about the common ForkJoinPool?
The common pool (ForkJoinPool.commonPool()) is shared across the entire JVM by parallel streams, default CompletableFutures, and many frameworks. Blocking IO on it stalls everything. You can adjust its size with the system property -Djava.util.concurrent.ForkJoinPool.common.parallelism=N, but this is a tuning aid, not a substitute for keeping IO off the common pool.
What are common interview follow-up questions?
- Should I ever use Executors factory methods in production? Prefer ThreadPoolExecutor directly for fixed pools with bounded queues. Scheduled is acceptable as-is for most use cases. - How do virtual threads change executor choice? Virtual threads with `Executors.newVirtualThreadPerTaskExecutor()` replace cached and fixed pools for IO-bound work, since blocking is effectively free. - Can you run a parallel stream on a custom ForkJoinPool? Yes, submit the stream operation as a task: `customPool.submit(() -> stream.parallel().map(...).collect(...)).join()`.
Visual Overview
Executor Choice Decision Tree
═════════════════════════════
Is the work CPU-bound recursive?
YES → ForkJoinPool (work-stealing)
NO
↓
Is the work periodic/timed?
YES → ScheduledThreadPool
NO
↓
Is the workload steady and bounded?
YES → ThreadPoolExecutor (bounded queue)
NO
↓
Short bursty tasks?
YES → Cached (with caution) or bounded pool
NO → ThreadPoolExecutor with IO-bound sizing
Key Takeaway
Java provides four executor shapes for different workloads. Fixed pools for steady bounded work with predictable resource usage. Cached pools for short bursty tasks that arrive in waves. Scheduled pools for timed and periodic execution. ForkJoinPool for CPU-bound recursive divide-and-conquer.
ForkJoinPool excels at CPU recursion because work-stealing keeps all cores busy. It fails at blocking IO because it is sized to roughly one thread per core, and blocked workers cannot steal or be stolen from. The common pool is shared JVM-wide, so blocking IO on it stalls all parallel streams and default CompletableFutures everywhere in the application.
In production, use ThreadPoolExecutor directly with bounded queues and rejection policies for fixed pools, and never use cached pools for slow or blocking tasks. Keep the ForkJoinPool common pool reserved for CPU-bound work and give blocking IO its own dedicated bounded executor.
At National Bank of India, parallel stream for VaR computation stalled because HTTP calls blocked common pool workers. Moving IO to a dedicated pool of 30 threads freed the common pool for CPU work, reducing VaR latency from 500ms to 50ms.
Remember one sentence: use fixed pools for steady work, scheduled for timed jobs, ForkJoinPool for CPU recursion, and never run blocking IO on the common pool.
---