How to create a parallel stream
In the Java Stream API, you can create a parallel stream using two primary idioms:
🟢 Junior Level
30-Second Summary
In the Java Stream API, you can create a parallel stream using two primary idioms:
- Directly from any
Collection: by invokingcollection.parallelStream(). - Transforming an existing sequential stream: by chaining the intermediate
.parallel()method on anyStream,IntStream,LongStream, orDoubleStream.
Both approaches exhibit identical runtime behavior: under the hood, they invoke StreamSupport.stream(spliterator, true), setting the pipeline’s internal parallelism flag to true. You can programmatically inspect whether a stream is operating in multi-threaded mode by calling stream.isParallel().
Basic Java 21 Examples
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class CreateParallelStreamDemo {
public static void main(String[] args) {
// Idiom 1: Directly from a Collection
List<String> names = List.of("Anna", "Bob", "Charlie", "Diana");
names.parallelStream()
.map(String::toUpperCase)
.forEach(System.out::println);
// Idiom 2: Transforming an existing stream via .parallel()
Stream.of("Red", "Green", "Blue")
.parallel()
.forEach(color -> System.out.printf("%s processed by %s%n",
color, Thread.currentThread().getName()));
// Idiom 3: Numerical primitive ranges
long sum = IntStream.rangeClosed(1, 1_000_000)
.parallel()
.sum();
System.out.println("Range sum: " + sum);
// Idiom 4: From an array using Arrays.stream()
int[] numbers = {10, 20, 30, 40, 50};
Arrays.stream(numbers)
.parallel()
.forEach(n -> System.out.print(n + " "));
}
}
🟡 Middle Level
Low-Level Instantiation: StreamSupport.stream()
The java.util.stream.StreamSupport utility class provides low-level static factories for creating stream pipelines from arbitrary data sources backed by a Spliterator:
public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel)
In fact, the default implementation of parallelStream() in the JDK’s java.util.Collection interface consists of a single line:
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
The “Last Call Wins” (All-or-Nothing) Rule
A common misconception is that a stream pipeline can execute certain intermediate operations in parallel while executing others sequentially. In the Java Stream API:
A stream pipeline is strictly all-or-nothing: it is either 100% sequential or 100% parallel.
List<Integer> list = List.of(1, 2, 3, 4, 5);
list.stream()
.parallel() // Request parallel mode
.filter(x -> x > 2)
.sequential() // Revert back to sequential mode!
.map(x -> x * 2)
.forEach(System.out::println);
In this pipeline, the entire stream executes sequentially from source to terminal operation.
The parallelism setting is stored as a single boolean state in the root pipeline head (AbstractPipeline.sourceStage.parallel). When a terminal operation is triggered, it queries this single property. The last configuration method invoked prior to terminal execution unconditionally overwrites the pipeline’s execution mode.
Stripping Encounter Order: .unordered()
When a stream source possesses the Spliterator.ORDERED characteristic (such as a List, LinkedHashSet, or an array), worker threads in ForkJoinPool must synchronize to ensure results conform strictly to upstream encounter order.
If output order does not matter for your business logic, inserting .unordered() strips this constraint:
List<String> largeList = getLargeDataset();
// unordered() permits worker threads to emit and combine elements without order synchronization
List<String> uniqueResults = largeList.parallelStream()
.unordered() // 🚀 Unlocks faster parallel distinct / limit / findAny execution
.distinct()
.toList();
Stripping order eliminates thread blocking during buffer consolidation, offering substantial throughput gains for stateful operations like distinct(), limit(), and skip().
🔴 Senior Level
Workload Isolation: Dedicated ForkJoinPool
By default, all parallel streams execute on the shared JVM-wide ForkJoinPool.commonPool(). To prevent long-running or computationally heavy streams from starving other microservice components, tasks can be encapsulated within a dedicated, isolated pool:
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.IntStream;
public class CustomForkJoinPoolDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
List<Integer> data = IntStream.rangeClosed(1, 100_000).boxed().toList();
// Dedicated worker pool with 4 threads
ForkJoinPool dedicatedPool = new ForkJoinPool(4);
try {
long total = dedicatedPool.submit(() ->
// Inside submit(), the executing thread is a worker belonging to dedicatedPool.
// AbstractPipeline detects this thread context via ForkJoinTask.getPool()!
data.parallelStream()
.filter(x -> x % 2 == 0)
.mapToLong(x -> (long) x * x)
.sum()
).get();
System.out.println("Result computed in custom pool: " + total);
} finally {
dedicatedPool.shutdown();
}
}
}
Pool Interception Mechanics:
When a parallel stream executes a terminal operation, AbstractPipeline schedules subtasks via ForkJoinTask.fork(). In OpenJDK HotSpot:
- If
Thread.currentThread()is an instance ofForkJoinWorkerThread, the task is submitted to that worker’s enclosing pool (Thread.currentThread().pool). - If called from an external thread (e.g.,
mainor a Tomcat HTTP thread), tasks fall back toForkJoinPool.commonPool().
[!WARNING] Specification Notice: Running parallel streams inside
customPool.submit()is an OpenJDK HotSpot implementation convention. It is not formally guaranteed by the Java SE Specification, though it is universally relied upon across modern production frameworks.
Global JVM Configuration & Container Quotas
The common pool size can be configured via JVM startup flags:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=8
By default, the parallelism level is initialized to Runtime.getRuntime().availableProcessors() - 1.
Docker & Kubernetes CPU Limits:
In containerized environments (Kubernetes pods), modern JVMs (Java 17/21) respect Linux cgroups CPU quotas (--cpus or resources.limits.cpu).
- If a container is restricted to
2CPUs,availableProcessors()reports2. - Consequently,
commonPool()initializes with only2 - 1 = 1background worker thread (plus the calling thread). - Under-provisioned containers gain minimal performance improvement from
parallelStream()and frequently suffer degraded performance due to thread coordination overhead.
4 Tricky Questions
1. What happens if a stream pipeline contains multiple alternating calls to .parallel() and .sequential()?
Answer: The stream executes either entirely in parallel or entirely sequentially based strictly on whichever method was called last before the terminal operation (“Last Call Wins”).
The Java Stream API does not support split-mode execution where some pipeline stages run concurrently while others run sequentially. Intermediate methods like .parallel() and .sequential() do not wrap stages; they simply toggle a boolean flag (parallel) on the root pipeline head (AbstractPipeline.sourceStage). When the terminal operation initiates, it reads this single flag once to determine the entire execution strategy.
2. Is there any performance or functional difference between list.parallelStream() and StreamSupport.stream(list.spliterator(), true)?
Answer: No. Functionally and performance-wise, they are 100% identical.
In the standard Java Collections Framework (java.util.Collection), the default implementation of parallelStream() is defined directly as:
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
Collection.parallelStream() is simply a convenient syntactic shortcut.
3. Why should you consider placing .unordered() before .parallel() on an ordered collection like an ArrayList?
Answer:
ArrayList intrinsically reports Spliterator.ORDERED. When executing in parallel, the stream engine must respect the encounter order of elements. For operations like .distinct(), .limit(), or collectors that preserve order:
- Worker threads cannot simply emit results as they finish; they must synchronize and buffer intermediate chunks into hierarchical order trees to reconstruct the original list sequence.
- Calling
.unordered()clears theORDEREDbitflag. Worker threads can then insert elements into hash tables, deduplicate, or emit items into downstream collectors using concurrent data structures (ConcurrentHashMap) without thread contention or order-preservation barriers.
4. How does running Java 21 inside a containerized environment (Docker/Kubernetes with CPU limits) impact the sizing and efficiency of ForkJoinPool.commonPool()?
Answer: Modern JVMs detect Linux cgroups quota limits. If a Kubernetes pod is configured with:
resources:
limits:
cpu: "2"
The JVM detects Runtime.getRuntime().availableProcessors() == 2. Since commonPool() defaults to availableProcessors() - 1, the pool will instantiate with exactly 1 worker thread.
With only 1 worker thread and 1 calling thread, there are only 2 threads available. The overhead of Spliterator.trySplit(), deque scheduling, and work-stealing often exceeds the multi-core computation benefits, resulting in code that is slower than a sequential loop while burning CPU cycles on thread management.
🎯 Interview Cheat Sheet
Parallel Stream Creation Matrix
| Idiom / Constructor | Code Syntax | Primary Use Case |
| :— | :— | :— |
| Collection.parallelStream() | list.parallelStream() | Idiomatic standard for Collections (List, Set) |
| Stream.parallel() | stream.parallel() | Adapting existing streams, ranges, or generator pipelines |
| Arrays.stream().parallel() | Arrays.stream(arr).parallel() | Direct parallelization over primitive or object arrays |
| StreamSupport.stream() | StreamSupport.stream(spltr, true)| Custom data structures and tailored Spliterator sinks |
| Custom ForkJoinPool | pool.submit(() -> s.parallel()...)| Isolating high-load CPU pipelines from commonPool |
Key Takeaways
- Two Main Ways:
collection.parallelStream()orstream.parallel(). - All-or-Nothing Execution: No partial parallelism exists; the last call to
.parallel()or.sequential()dictates the entire pipeline. unordered()Advantage: Stripping order with.unordered()eliminates synchronization bottlenecks in stateful parallel operations.- Dedicated Pool Pattern: Use
customPool.submit(() -> stream.parallel()...)to isolate heavy computations from the shared JVMcommonPool.
Red Flags & Anti-Patterns to Avoid
- ❌ Assuming partial pipeline parallelism: Writing
.parallel().filter(...).sequential().map(...)does NOT run filter in parallel and map sequentially; the entire stream runs sequentially. - ❌ Leaking custom
ForkJoinPoolinstances: Forgetting to callcustomPool.shutdown()results in severe thread and memory leaks. - ❌ Modifying
-Djava.util.concurrent.ForkJoinPool.common.parallelismin shared services: Alters global JVM threading behavior, destabilizing external libraries and frameworks. - ❌ Relying on
parallelStream()in 1-core or 2-core containers: Container CPU throttling leavescommonPool()with 0 or 1 worker thread, rendering parallel execution counter-productive.