☕ Java

Parallel Streams

Parallel streams, introduced in Java 8 alongside the Streams API, allow a stream pipeline to split its source data into segments processed concurrently across multiple threads from a shared fork/join thread pool, then combine the partial results, with the goal of utilizing multiple CPU cores for data-parallel computation with minimal code change from the sequential version. They are created via Collection.parallelStream() or stream.parallel(), and internally rely on the Spliterator abstraction to divide work and the common ForkJoinPool to execute it. This entry covers how parallel streams actually execute under the hood, when parallelism helps versus hurts performance, the strict requirements around statelessness and side effects for correctness, and common pitfalls including thread-pool starvation and incorrect assumptions about ordering.

How Parallel Streams Execute — Spliterator and the Common ForkJoinPool

A parallel stream does not create new dedicated threads for each operation; instead, by default, it submits its work to the shared, JVM-wide ForkJoinPool.commonPool(), the same pool used internally by CompletableFuture's async methods and parallel array operations like Arrays.parallelSort(). This pool is sized, by default, to the number of available processor cores minus one (the calling thread itself participates as a worker), which means the degree of parallelism available to any single parallel stream operation is implicitly limited by, and shared with, every other concurrent use of the common pool across the entire application. Splitting the source data for parallel processing is handled by the Spliterator (a portmanteau of "splittable iterator") associated with the stream's source. Spliterator defines a trySplit() method that attempts to partition the remaining elements into two roughly equal chunks, each of which can be further split recursively until the chunks are small enough (governed by an internal heuristic threshold) to process sequentially on a single thread, following a fork/join divide-and-conquer pattern. Different source types have very different splitting characteristics: an ArrayList or array has an efficient, near-constant-time spliterator because elements are contiguous and index-accessible, making it split cheaply and evenly; a LinkedList must walk node-by-node to find a split point, making splitting comparatively expensive; and a stream produced from a non-sized source like Stream.iterate(...) without a limit, or from many I/O-based sources, may not be efficiently splittable at all, in which case parallelizing it provides little or no benefit and can even be slower than the sequential equivalent due to coordination overhead.
Java
// ── Creating a parallel stream ──────────────────────────────────────────
List<Integer> numbers = IntStream.rangeClosed(1, 10_000_000).boxed().collect(Collectors.toList());

long sumSequential = numbers.stream()
    .mapToLong(Integer::longValue)
    .sum();

long sumParallel = numbers.parallelStream()           // from a Collection
    .mapToLong(Integer::longValue)
    .sum();

long sumParallel2 = numbers.stream()
    .parallel()                                         // converts an existing stream
    .mapToLong(Integer::longValue)
    .sum();

// ── Checking and influencing the common pool size ───────────────────────
System.out.println(Runtime.getRuntime().availableProcessors());
// Common pool parallelism defaults to availableProcessors() - 1
// (can be overridden via -Djava.util.concurrent.ForkJoinPool.common.parallelism=N,
//  but this affects the ENTIRE application's shared pool, not just one stream)

// ── Spliterator splitting behavior differs by source type ──────────────
Spliterator<Integer> arrayListSplit = new ArrayList<>(numbers).spliterator();  // cheap, even splits
Spliterator<Integer> linkedListSplit = new LinkedList<>(numbers).spliterator(); // expensive, node-walking splits

// Poorly splittable source — parallelizing this offers little/no benefit:
Stream<Integer> iterateStream = Stream.iterate(0, i -> i + 1).limit(1_000_000).parallel();

When Parallelism Helps vs Hurts — Sizing, Source, and Operation Cost

Parallel streams are not a free performance upgrade and frequently make small or I/O-bound workloads slower, not faster, due to the fixed overhead of splitting the source, distributing and scheduling tasks across the fork/join pool, waiting for all subtasks to complete, and merging partial results. This overhead must be paid regardless of whether the computation per element is cheap or expensive, and for small collections or cheap per-element operations, this overhead dominates and a sequential stream completes faster than the equivalent parallel one, often by a substantial margin. The conditions under which parallel streams tend to genuinely help are well characterized: a large enough data set (commonly cited rules of thumb suggest tens of thousands of elements or more, though the actual break-even point depends heavily on per-element cost), a source that splits efficiently (array-backed or ArrayList-backed sources, or IntStream.range, rather than LinkedList or I/O-derived sources), and a per-element operation that is computationally substantial relative to the splitting/merging overhead (CPU-bound work like complex calculations, rather than trivial operations like a single addition or a cheap field access). Parallel streams are a poor fit for I/O-bound operations — reading files, making network calls, or querying a database per element — because the common ForkJoinPool is sized for CPU-bound work (cores - 1 threads) and blocking I/O on those threads stalls the entire shared pool for any other concurrent parallel stream or CompletableFuture work in the application, a problem severe enough that it has a name: common pool starvation. For I/O-bound parallel work, a dedicated, separately-sized thread pool (e.g. via CompletableFuture.supplyAsync(...) with an explicit Executor) is the appropriate tool, not parallel streams.
Java
// ── Overhead dominates for small/cheap workloads — parallel can be SLOWER
List<Integer> small = IntStream.rangeClosed(1, 100).boxed().collect(Collectors.toList());

long startSeq = System.nanoTime();
long sumSeq = small.stream().mapToLong(i -> i).sum();
long seqTime = System.nanoTime() - startSeq;

long startPar = System.nanoTime();
long sumPar = small.parallelStream().mapToLong(i -> i).sum();
long parTime = System.nanoTime() - startPar;
// For a workload this small, parTime is very commonly LARGER than seqTime —
// splitting/scheduling/merging overhead outweighs any parallel speedup

// ── Where parallelism genuinely helps — large data, expensive per-element work
List<BigInteger> largeNumbers = generateMillionsOfLargeNumbers();

boolean anyPrime = largeNumbers.parallelStream()
    .anyMatch(JavaParallelStreams::isProbablePrime);   // expensive CPU-bound check per element,
                                                          // large source, array-backed — good fit

// ── PITFALL: blocking I/O inside a parallel stream starves the common pool
List<String> urls = getThousandsOfUrls();

// BAD — blocks shared ForkJoinPool threads on network I/O, starving the
// whole application's other parallel-stream and CompletableFuture work:
List<String> results = urls.parallelStream()
    .map(url -> blockingHttpGet(url))   // I/O-bound — wrong tool for the job
    .collect(Collectors.toList());

// BETTER — use a dedicated executor sized for I/O-bound concurrency instead:
ExecutorService ioPool = Executors.newFixedThreadPool(50);
List<CompletableFuture<String>> futures = urls.stream()
    .map(url -> CompletableFuture.supplyAsync(() -> blockingHttpGet(url), ioPool))
    .collect(Collectors.toList());
List<String> results2 = futures.stream().map(CompletableFuture::join).collect(Collectors.toList());
ioPool.shutdown();

Correctness Requirements — Statelessness, Side Effects, and Ordering

Because parallel stream operations may execute concurrently on different threads operating on different chunks of the source, the lambdas passed to stream operations (map, filter, forEach, reduce, collect, etc.) must satisfy strict correctness requirements that are easy to violate accidentally and that the compiler does not check. Functions passed to map, filter, and similar operations must be non-interfering (must not modify the stream's data source while the pipeline is executing) and, for parallel correctness, generally stateless — they should not read or write mutable state shared across invocations, because concurrent invocations on different threads can race on that shared state. A frequent and subtle bug is using forEach with a lambda that mutates a shared, non-thread-safe collection or counter from outside the stream, such as adding to an ArrayList or incrementing a plain int/long counter across threads. This produces data races, lost updates, or ConcurrentModificationException, with the failure often being intermittent and load-dependent rather than consistently reproducible, making it especially dangerous in production. The correct approach for accumulating results from a stream — sequential or parallel — is to use the stream's own collecting operations (collect(Collectors.toList()), reduce(...), or thread-safe concurrent collectors), which are specifically designed to combine partial results from different threads correctly, rather than manually mutating an external collection inside forEach. Ordering is another frequent source of confusion: forEach does not guarantee any processing order for parallel streams (elements may be processed in any order across threads), while forEachOrdered enforces encounter order at the cost of giving up much of the parallelism benefit, since it must synchronize to preserve sequence. Operations like findFirst, sorted, and limit also have different — generally higher — costs in parallel pipelines than their sequential counterparts, because preserving the stream's defined ordering semantics requires additional coordination across the parallel chunks.
Java
// ── PITFALL: mutating shared external state from forEach — data race ──
List<Integer> results = new ArrayList<>();   // NOT thread-safe
numbers.parallelStream().forEach(n -> {
    if (n % 2 == 0) {
        results.add(n);   // BUG: concurrent adds from multiple threads — race condition,
                            // can throw ConcurrentModificationException or silently drop elements
    }
});

// ── CORRECT: use the stream's own collecting operations ────────────────
List<Integer> correctResults = numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());   // designed to safely merge per-thread partial results

// ── PITFALL: a shared, non-atomic counter mutated inside the pipeline ──
int[] counter = {0};   // effectively shared mutable state
numbers.parallelStream().forEach(n -> counter[0]++);   // BUG: lost updates under concurrency
System.out.println(counter[0]);   // unreliable — often less than numbers.size()

// ── CORRECT: let the stream compute the count itself ────────────────────
long count = numbers.parallelStream().count();          // or use an AtomicLong if accumulating manually

// ── Ordering: forEach has no order guarantee in parallel; forEachOrdered does
numbers.parallelStream().forEach(System.out::println);          // arbitrary order across threads
numbers.parallelStream().forEachOrdered(System.out::println);   // preserves encounter order,
                                                                    // but loses most parallel benefit

// ── findFirst/sorted/limit are comparatively more expensive in parallel ─
Optional<Integer> first = numbers.parallelStream()
    .filter(n -> n > 1000)
    .findFirst();   // must coordinate across chunks to honor encounter-order "first" semantics
// findAny() is cheaper in parallel when any matching element (not specifically the first) suffices

Related Topics in Java 8 Features

Lambda Expressions
Lambda expressions, introduced in Java 8, are anonymous functions — blocks of code that can be stored in variables, passed as arguments, and returned from methods, treating behavior as data. A lambda has three parts: a parameter list, an arrow token (->), and a body. The body is either a single expression (whose value is the implicit return value) or a block of statements wrapped in braces. Lambdas implement functional interfaces — interfaces with exactly one abstract method — allowing any lambda whose signature matches the abstract method's signature to be used wherever that interface is expected. The lambda syntax is syntactic sugar: every lambda is compiled to an invocation of the functional interface's abstract method, with the compiler generating a class (via invokedynamic) that implements the interface and delegates to the lambda body. This entry covers the complete lambda syntax including all shorthand forms, variable capture and the effectively-final constraint, method references as a specialized lambda syntax, the relationship between lambdas and the type system, how lambdas interact with exception handling, the invokedynamic compilation strategy and its performance characteristics, and the complete set of rules governing lambda type inference.
Functional Interfaces
A functional interface is any Java interface that has exactly one abstract method. This single-abstract-method (SAM) contract makes the interface a valid target type for a lambda expression or method reference — the lambda provides the implementation of that one abstract method. The @FunctionalInterface annotation is optional but strongly recommended: it causes the compiler to verify that the interface satisfies the SAM constraint, rejecting it at compile time if there is more than one abstract method. The java.util.function package, introduced in Java 8, provides 43 standard functional interfaces organized around four root types — Function, Consumer, Supplier, Predicate — and their variations for primitives (IntFunction, LongSupplier, DoubleConsumer, etc.), binary operations (BiFunction, BiConsumer, BiPredicate), and unary operators (UnaryOperator, IntUnaryOperator, etc.). This entry covers the design principles behind functional interfaces, the complete @FunctionalInterface contract including default and static methods, the full java.util.function hierarchy and the pattern that governs naming, creating custom functional interfaces with checked exceptions, composing functional interfaces via default methods, and the relationship between functional interfaces and the type system including the rules for lambda assignment and widening.
Predicate
Predicate<T> is a functional interface in java.util.function representing a boolean-valued function of one argument, with the single abstract method boolean test(T t). It is one of the four foundational functional interfaces in the Java standard library and is used throughout the Collections framework, Streams API, and Optional for filtering, condition testing, and validation. Predicate is designed for composition: its default methods and(Predicate), or(Predicate), and negate() allow building complex boolean expressions from simple predicates without boilerplate. The static methods isEqual(Object) and not(Predicate) provide factory methods for common cases. The primitive specializations IntPredicate, LongPredicate, and DoublePredicate avoid boxing overhead for numeric values. BiPredicate<T,U> extends the concept to two-argument boolean functions. This entry covers the complete Predicate API, all composition methods and their short-circuit semantics, the static factory methods, primitive specializations, BiPredicate, using Predicate in stream pipelines and Collections methods, building validation frameworks with Predicate composition, and the performance and readability trade-offs of different composition styles.
Function
Function<T,R> is a functional interface in java.util.function representing a function that accepts one argument of type T and produces a result of type R, with the single abstract method R apply(T t). It is the most general transformation interface in the standard library, used throughout the Streams API for mapping (Stream.map()), in Optional for value transformation (Optional.map(), Optional.flatMap()), and as a building block for more specialized functional interfaces. Function provides two default composition methods — andThen() and compose() — that create new functions by chaining two functions together, enabling functional pipeline construction without intermediate variables. The specializations cover all combinations of generic and primitive inputs and outputs: ToIntFunction, IntFunction, IntToLongFunction, and so on. UnaryOperator<T> extends Function<T,T> for operations that transform a value within the same type. BiFunction<T,U,R> generalizes to two input arguments. This entry covers the complete Function API, the semantics of andThen versus compose, all specializations and when each is appropriate, the functional relationship between Function and other java.util.function types, partial application patterns, and Function as the basis for building data pipelines.