Definition
- Default pool –
ForkJoinPool.commonPool()with parallelism =Runtime.availableProcessors() - 1(typically 3 on a 4-core machine). - Shared – the same pool is used by all
CompletableFutureasync methods,parallelStream(),Arrays.parallelSort(), and many frameworks. - Small – designed for short CPU-bound tasks, not blocking IO.
- No control – unnamed threads, no bounded queues, no rejection policies, no monitoring.
- Fix – always pass a dedicated
ExecutorService:supplyAsync(task, ioPool)for IO,supplyAsync(task, cpuPool)for compute. - Named threads – use custom ThreadFactory for debugging: thread dumps show
io-worker-5instead ofForkJoinPool.commonPool-worker-1.
ForkJoinPool.commonPool() is like National Bank of India having one shared car for all departments. Delivery, executive travel, and IT errands all share 3 cars. If delivery takes all 3 for long trips, executives cannot travel and the entire bank is stalled. Dedicated executors give each department its own fleet: delivery gets 20 vans, executives get 3 sedans, IT gets 5 bikes.
Understanding the Concepts
What is the default pool for supplyAsync?
When you call supplyAsync() or runAsync() without specifying an executor, the task runs on ForkJoinPool.commonPool(). This pool has parallelism equal to Runtime.getRuntime().availableProcessors() - 1.
System.out.println("Common pool parallelism: " +
ForkJoinPool.commonPool().getParallelism());
// On a 4-core machine: 3
// On an 8-core machine: 7
Why is the common pool dangerous for IO-bound work?
The common pool is shared across the entire JVM. Blocking IO on its few threads starves all other users of the pool.
// Component A: 3 slow JDBC queries on commonPool
for (int i = 0; i < 3; i++) {
CompletableFuture.supplyAsync(() -> {
return jdbcTemplate.queryForObject("SELECT ...", String.class);
// Blocks 2 seconds! All 3 commonPool threads occupied!
});
}
// Component B (UNRELATED): parallel stream for reports
report = accounts.parallelStream() // ALSO uses commonPool!
.map(this::generateStatement) // STALLED! All threads blocked by A!
.collect(Collectors.toList());
// Report generation waits 2 seconds because JDBC monopolized the pool!
How do you create dedicated executors?
package com.bank.core;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class PoolFactory {
private static final AtomicInteger ioCounter = new AtomicInteger();
private static final AtomicInteger cpuCounter = new AtomicInteger();
// IO pool: many threads for blocking calls
public static ExecutorService createIoPool() {
return new ThreadPoolExecutor(
20, 50, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
r -> {
Thread t = new Thread(r, "io-worker-" + ioCounter.incrementAndGet());
t.setDaemon(true);
return t;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
// CPU pool: cores threads for compute
public static ExecutorService createCpuPool() {
return Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors(),
r -> new Thread(r, "cpu-worker-" + cpuCounter.incrementAndGet())
);
}
}
How do you use dedicated pools with CompletableFuture?
ExecutorService ioPool = PoolFactory.createIoPool();
ExecutorService cpuPool = PoolFactory.createCpuPool();
// IO work on dedicated pool
CompletableFuture.supplyAsync(() -> httpCall(), ioPool);
CompletableFuture.supplyAsync(() -> jdbcQuery(), ioPool);
// CPU work on dedicated pool
CompletableFuture.supplyAsync(() -> encrypt(data), cpuPool);
// commonPool stays free for parallelStream and framework use
What are the benefits of named threads?
Named threads make thread dumps instantly readable. Instead of ForkJoinPool.commonPool-worker-1, you see io-worker-5 or payment-worker-3, immediately telling you which pool and which concern is involved.
What about Spring Boot configuration?
// @Bean
// public Executor taskExecutor() {
// ThreadPoolTaskExecutor e = new ThreadPoolTaskExecutor();
// e.setCorePoolSize(20);
// e.setMaxPoolSize(50);
// e.setQueueCapacity(1000);
// e.setThreadNamePrefix("async-");
// e.setRejectedExecutionHandler(new CallerRunsPolicy());
// return e;
// }
// Usage: CompletableFuture.supplyAsync(() -> fetch(), taskExecutor);
How can you override the common pool's parallelism?
Use the system property: -Djava.util.concurrent.ForkJoinPool.common.parallelism=16. This affects the entire JVM. Prefer dedicated pools over changing the global setting.
What are common mistakes?
// WRONG: blocking IO on commonPool
CompletableFuture.supplyAsync(() -> httpCall()); // blocks commonPool thread!
// WRONG: unbounded queue with no rejection policy
new ThreadPoolExecutor(10, 10, 0, SECONDS, new LinkedBlockingQueue<>());
// Queue grows without limit, OOM risk!
// CORRECT: dedicated pool with bounds and policy
CompletableFuture.supplyAsync(() -> httpCall(), ioPool); // dedicated pool!
What are common interview follow-up questions?
- Can you change commonPool at runtime? No, only via system properties at JVM start.
- What is CallerRunsPolicy? When the queue is full, the submitting thread runs the task itself, providing natural back-pressure.
- Does parallelStream use the same pool? Yes, parallelStream uses commonPool by default.
Visual Overview
Default: everything on commonPool (DANGER) ┌─────────────────────────────────────────┐ │ ForkJoinPool.commonPool (3 threads) │ │ │ │ supplyAsync() ──▶ worker-1 │ │ parallelStream() ──▶ worker-2 │ │ framework async ──▶ worker-3 │ │ │ │ All compete for 3 threads! │ └─────────────────────────────────────────┘ Fix: dedicated pools (SAFE) ┌─────────────────┐ ┌─────────────────┐ │ ioPool (50) │ │ cpuPool (4) │ │ io-worker-1..50 │ │ cpu-worker-1..4 │ │ HTTP, JDBC, IO │ │ encrypt, sort │ └─────────────────┘ └─────────────────┘ ┌─────────────────┐ │ commonPool (3) │ ← free for parallelStream └─────────────────┘
Key Takeaway
supplyAsync() defaults to ForkJoinPool.commonPool(), a shared, JVM-wide pool with only cores - 1 threads. In production, this is dangerous because the pool is shared by all CompletableFutures, parallel streams, and many frameworks. Blocking IO on its threads starves everything.
Always pass a dedicated ExecutorService. Separate IO work (many threads, named io-worker-N, bounded queue with CallerRunsPolicy) from CPU work (cores threads, named cpu-worker-N). This leaves commonPool free for parallel streams and framework use. Named threads make thread dumps readable. Bounded queues prevent OOM.
At National Bank of India, payment gateway calls (200ms blocking) ran on commonPool's 3 threads. 100 payments took 7 seconds. Moving to a dedicated 50-thread IO pool cut that to 400ms. The commonPool was freed for reporting parallel streams.
Remember one sentence: never use the default commonPool for blocking IO; always pass a dedicated executor.
---