Definition
- commonPool – JVM-wide shared pool with
cores - 1threads, designed for short CPU-bound work. - Shared users –
CompletableFuture.supplyAsync(),parallelStream(),Arrays.parallelSort(), and many frameworks all use the same pool. - Resource starvation – blocking IO (JDBC, HTTP) on commonPool threads prevents any other work from executing.
- Isolation failure – one component's slow dependency cascades to unrelated components sharing the pool.
- Fix – dedicated
ExecutorServiceper concern: IO pool (many threads), CPU pool (cores), commonPool left free.
The commonPool is National Bank of India's only conference room with 3 seats. Department A books it for a 2-hour meeting (blocking JDBC). Department B needs a 5-minute standup: wait 2 hours. Department C needs to review reports: wait. One slow department blocks everyone because they share the same room. Dedicated pools give each department their own room.
Understanding the Concepts
Why is blocking IO on commonPool dangerous?
ForkJoinPool.commonPool() has only cores - 1 threads. These threads are shared across the entire JVM. When blocking IO consumes all threads, nothing else can run. Every CompletableFuture, every parallel stream, and every framework that uses commonPool is stalled.
package com.bank.concurrency;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class CommonPoolDangerDemo {
// Component A: 3 slow JDBC queries on commonPool
public void componentA() {
for (int i = 0; i < 3; i++) {
CompletableFuture.supplyAsync(() -> {
return jdbcQuery("SELECT * FROM settlements WHERE status = 'PENDING'");
// blocks 2 seconds!
});
}
// All 3 commonPool threads blocked for 2 seconds!
}
// Component B: parallel stream (UNRELATED, also uses commonPool)
public void componentB() {
var statements = accounts.parallelStream()
.map(this::generateStatement) // No threads available!
.collect(Collectors.toList());
// STALLED until Component A's JDBC calls finish
}
// Component C: CF notification (UNRELATED)
public void componentC() {
CompletableFuture.supplyAsync(() -> sendNotification());
// QUEUED behind Component A's blocking calls!
}
}
Who shares commonPool?
| User | How it uses commonPool |
|------|------------------------|
| CompletableFuture.supplyAsync() (no executor) | Default executor |
| parallelStream() | Fork-join tasks |
| Arrays.parallelSort() | Parallel merge-sort |
| Spring WebFlux internals | Some async callbacks |
| Third-party libraries | Often undocumented |
All compete for the same few threads. A single blocking component can silently degrade the entire application.
How do you fix it with dedicated pools?
package com.bank.concurrency;
import java.util.concurrent.*;
public class DedicatedPoolDemo {
// Dedicated pools per concern
private static final ExecutorService jdbcPool = new ThreadPoolExecutor(
20, 20, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500),
r -> { Thread t = new Thread(r, "jdbc-pool-" + t.getId()); return t; },
new ThreadPoolExecutor.CallerRunsPolicy()
);
private static final ExecutorService notifyPool = Executors.newFixedThreadPool(10,
r -> { Thread t = new Thread(r, "notify-pool"); return t; });
public void isolatedCalls() {
CompletableFuture.supplyAsync(() -> jdbcQuery(), jdbcPool); // on jdbcPool
CompletableFuture.supplyAsync(() -> sendNotif(), notifyPool); // on notifyPool
accounts.parallelStream().map(this::generateStatement); // commonPool free!
}
}
What is the Spring Boot approach?
@Bean("jdbcExecutor")
public Executor jdbcExecutor() {
ThreadPoolTaskExecutor e = new ThreadPoolTaskExecutor();
e.setCorePoolSize(20);
e.setMaxPoolSize(50);
e.setQueueCapacity(1000);
e.setThreadNamePrefix("jdbc-");
e.setRejectionPolicy(new ThreadPoolExecutor.CallerRunsPolicy());
return e;
}
@Async("jdbcExecutor")
public CompletableFuture<SettlementData> fetchSettlementData(String tradeId) {
return CompletableFuture.completedFuture(settlementRepo.findByTradeId(tradeId));
}
Can you increase commonPool's size?
Yes, via the system property java.util.concurrent.ForkJoinPool.common.parallelism. But this is a workaround, not a fix. Increasing the pool size just delays the problem because blocking tasks can always fill any pool. Dedicated pools with proper isolation is the correct approach.
What is the rule?
supplyAsync(task) with no pool = commonPool = dangerous for blocking IO. supplyAsync(task, ioPool) = dedicated pool = safe. Every blocking call (JDBC, HTTP, gRPC, file IO) must specify a dedicated executor.
What are edge cases?
Work-stealing does not help with blocking. ForkJoinPool's work-stealing assumes tasks complete quickly and return to the deque. A blocked thread cannot steal from others and cannot be stolen from. The pool effectively loses that worker for the duration of the blocking call.
What are common interview follow-up questions?
- Can parallelStream use a custom pool? Yes, submit it inside a custom ForkJoinPool: `customPool.submit(() -> stream.parallel()...)`. The stream operations run on that custom pool's workers. - How do virtual threads change this? Virtual threads make blocking cheap because the JVM unmounts a blocked virtual thread from its carrier. But until virtual threads are widely adopted, dedicated platform-thread pools remain the standard approach.
Visual Overview
commonPool (3 threads, shared) ┌─────────────────────────────────────────┐ │ Thread-1: JDBC query (2s BLOCKED) │ │ Thread-2: JDBC query (2s BLOCKED) │ │ Thread-3: JDBC query (2s BLOCKED) │ │ │ │ parallelStream: QUEUED (no threads!) │ │ supplyAsync: QUEUED (no threads!) │ │ framework: QUEUED (no threads!) │ └─────────────────────────────────────────┘ Everything stalls for 2 seconds!
Key Takeaway
Running blocking IO on ForkJoinPool.commonPool() is a resource isolation failure. The pool has only cores - 1 threads and is shared by all CompletableFutures, parallel streams, and many frameworks. Blocking all threads with database queries or HTTP calls starves everything in the JVM. One slow dependency cascades to affect the entire application.
The fix is dedicated executors per concern. IO pool with many threads for blocking calls. CPU pool with cores threads for computation. CommonPool left free for parallel streams and lightweight tasks. Named threads for debugging. Bounded queues for back-pressure.
At National Bank of India, dashboard widgets making JDBC calls blocked commonPool, stalling the reporting parallel streams. Customers waited 2 extra seconds for reports. Moving JDBC to a dedicated IO pool eliminated the cross-contamination.
Remember one sentence: blocking IO on commonPool starves everything; use dedicated pools per concern.
---