Custom thread pools for parallel streams
How to run a parallel stream inside a custom ForkJoinPool, why it works in current JDKs, where the specification stops, and when executors or virtual threads are better.
On this page
🧰 What is a custom thread pool for a parallel stream?
A custom thread pool means you submit the code that runs a parallel stream to your own ForkJoinPool instead of letting the request thread enter the JVM-wide common pool. The common pattern is pool.submit(() -> list.parallelStream()...).get(). Think of it as moving a counting job from the shared office desk to a reserved room with exactly four seats.
For this lesson, we'll focus on the practical pattern, the implementation detail that makes it work in current JDKs, and the cases where you should not use it. Read Parallel streams: when they help and hurt first if the NQ model, source splitting and ordered-operation costs are not fresh. ForkJoinPool internals are covered in the Concurrency module.
Below the line (out of scope)
- We will use
ForkJoinPool, because parallel streams are built on fork-join tasks. - We will not pretend a custom pool fixes poor splitting, blocking I/O or shared mutable state.
- We will show Java 21 cleanup.
ForkJoinPoolimplementsAutoCloseablesince Java 19. - We will compare with
CompletableFuture,ExecutorServiceand virtual threads for non-stream workloads.
The 30-second answer
You can run a parallel stream in a custom pool by submitting the whole stream computation to new ForkJoinPool(n) and waiting for the result: pool.submit(() -> list.parallelStream().map(...).toList()).get(). In current OpenJDK, parallel stream tasks fork into the pool of the current fork-join worker thread, so the nested stream uses your pool rather than the common pool. The catch is that the Stream API does not specify this as a portable guarantee. Close the pool, preferably with try-with-resources on Java 19+, and do not use this pattern for blocking I/O. For blocking calls, use a normal executor, Java 21 virtual threads, or CompletableFuture with a custom executor.
What you’ll learn
- Run a parallel stream inside
new ForkJoinPool(n).submit(...).get()safely - Explain why nested stream tasks use the current fork-join worker pool in today's JDK implementation
- Name the limit: this behavior is implementation behavior, not a
Streamspecification guarantee - Close custom pools correctly, including
try-with-resources since Java 19 - Use
java.util.concurrent.ForkJoinPool.common.parallelismwhen the common pool really must be tuned - Choose
ExecutorService, virtual threads orCompletableFuturefor blocking I/O instead of parallel streams
01Picture it first: reserve a room, don't own the building
The common pool is like the office kitchen table. Everyone can use it, so one team spreading papers everywhere slows down other teams. A custom ForkJoinPool is a reserved room: your job has a fixed number of seats, and unrelated common-pool work is less likely to be disturbed.
But the work is still fork-join work. A custom pool changes where tasks run, not whether the stream is a good parallel candidate. Bad source splitting and expensive merges stay bad.
A request submits a parallel stream computation to a custom ForkJoinPool, whose worker threads split and process chunks instead of using the common pool
Notice the boundary. The request thread waits on the submitted task. Inside the pool, a worker starts the terminal operation, and stream subtasks are forked by that worker.
02Running a parallel stream in a custom ForkJoinPool
Here is the pattern I use when I really need it. Keep the pool lifetime obvious. Keep the stream computation inside the submitted lambda. Return a value instead of mutating shared state outside the pool.
Try it yourself first
Before reading the notes, predict which thread names should appear: ForkJoinPool.commonPool-worker-* or a worker from the custom pool.
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Why it works, and where the spec stops
- Advanced: common-pool tuning and blocking I/O
- CompletableFuture with a custom executor
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal