🚀AdvancedAdvanced Streams

Parallel Streams

Use parallel streams for data-parallel computation and understand when they help versus hurt performance.

Overview

Parallel streams split the data source into chunks and process each chunk concurrently using threads from ForkJoinPool.commonPool(). They are designed for CPU-bound, stateless operations on large data sets where the computation cost per element outweighs the parallelism overhead. Adding .parallel() (or using .parallelStream()) is syntactically trivial, but parallel streams are not always faster. For small data sets, I/O-bound work, or stateful operations, they can be slower than sequential due to thread management overhead, synchronization, and cache effects. Understanding the trade-offs is what separates effective use of parallel streams from naive over-use.

Key Points

  • •stream().parallel() or collection.parallelStream() enables parallelism
  • •sequential() converts a parallel stream back to sequential
  • •Parallel streams use ForkJoinPool.commonPool() by default
  • •Use a custom ForkJoinPool by submitting the stream operation as a task
  • •Operations must be stateless and non-interfering for correct parallel results
  • •Reduction operations (reduce, collect) must be associative and non-interfering
  • •findFirst() is slower on parallel streams; use findAny() if order does not matter
  • •Ordered parallel operations (like limit, sorted, findFirst) may need merging, adding overhead
  • •Avoid shared mutable state; use thread-safe collectors instead
  • •Good candidates: large arrays, CPU-bound stateless maps, aggregate reductions

Examples

Basic parallel stream and performance comparison

import java.util.stream.LongStream;

public class Main {
    static long computeSquareSum(long n, boolean parallel) {
        LongStream stream = LongStream.rangeClosed(1, n);
        if (parallel) stream = stream.parallel();
        return stream.map(i -> i * i).sum();
    }

    public static void main(String[] args) {
        long n = 50_000_000L;

        long t1 = System.currentTimeMillis();
        long seqResult = computeSquareSum(n, false);
        long seqTime = System.currentTimeMillis() - t1;

        long t2 = System.currentTimeMillis();
        long parResult = computeSquareSum(n, true);
        long parTime = System.currentTimeMillis() - t2;

        System.out.println("Sequential: " + seqTime + "ms, result=" + seqResult);
        System.out.println("Parallel:   " + parTime + "ms, result=" + parResult);
        System.out.println("Results match: " + (seqResult == parResult));
    }
}

On a multi-core machine with large n, parallel should be faster. With small n (< 10,000), sequential is usually faster due to overhead. Always benchmark with your actual data size.

Thread safety: using a thread-safe collector

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.*;

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("alpha", "beta", "gamma", "alpha", "beta", "delta");

        // WRONG: mutable shared HashMap is not thread-safe
        // Map<String, Long> bad = new HashMap<>();
        // words.parallelStream().forEach(w -> bad.merge(w, 1L, Long::sum)); // race condition

        // CORRECT: use a thread-safe collector
        Map<String, Long> freq = words.parallelStream()
            .collect(Collectors.groupingByConcurrent(w -> w, Collectors.counting()));

        System.out.println(freq);
    }
}

groupingByConcurrent is the parallel-friendly version of groupingBy. It uses ConcurrentHashMap internally. Regular groupingBy also works in parallel streams but merges results per chunk, which is safe but does extra work.

Custom thread pool for isolated parallel computation

import java.util.concurrent.ForkJoinPool;
import java.util.stream.LongStream;

public class Main {
    public static void main(String[] args) throws Exception {
        ForkJoinPool pool = new ForkJoinPool(4); // 4-thread pool

        long result = pool.submit(() ->
            LongStream.rangeClosed(1, 10_000_000)
                .parallel()
                .sum()
        ).get();

        pool.shutdown();
        System.out.println("Sum: " + result);
    }
}

Submitting to a custom ForkJoinPool ensures parallel stream work runs in that pool instead of the shared common pool. Use this to isolate CPU-intensive tasks from other application components using the common pool.

Common Mistakes

  • Using parallel streams for small data sets. The overhead of splitting, scheduling, and merging results is larger than the computation itself for small inputs. Benchmark first.
  • Using parallel streams with blocking I/O. Blocking a ForkJoin thread stalls other tasks. Use async I/O or ensure ForkJoin managed blocking via ForkJoinPool.managedBlock().
  • Mutating shared state inside parallel stream operations. HashMap is not thread-safe; multiple threads updating it concurrently causes data corruption. Use thread-safe collectors.
  • Expecting findFirst() to be fast on parallel streams. findFirst() requires the first element in encounter order, which may not be the first found. Use findAny() if order does not matter.

Note

A rough rule: parallel streams are worth considering for CPU-bound operations on collections with 10,000+ elements. For fewer elements, sequential is usually faster. Always profile in your actual runtime environment rather than assuming parallel is better.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

Which thread pool do parallel streams use by default?