🌊 Section 8 · Question #11

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:

  1. Directly from any Collection: by invoking collection.parallelStream().
  2. Transforming an existing sequential stream: by chaining the intermediate .parallel() method on any Stream, IntStream, LongStream, or DoubleStream.

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 of ForkJoinWorkerThread, the task is submitted to that worker’s enclosing pool (Thread.currentThread().pool).
  • If called from an external thread (e.g., main or a Tomcat HTTP thread), tasks fall back to ForkJoinPool.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 2 CPUs, availableProcessors() reports 2.
  • Consequently, commonPool() initializes with only 2 - 1 = 1 background 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 the ORDERED bitflag. 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() or stream.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 JVM commonPool.

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 ForkJoinPool instances: Forgetting to call customPool.shutdown() results in severe thread and memory leaks.
  • ❌ Modifying -Djava.util.concurrent.ForkJoinPool.common.parallelism in 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 leaves commonPool() with 0 or 1 worker thread, rendering parallel execution counter-productive.