Index

Parallel Streams Basics

Java Streams

9.1 Introduction to Parallel Streams

Parallel streams provide a simple and powerful way to perform concurrent data processing in Java. By splitting a stream’s workload across multiple threads, parallel streams can leverage multi-core processors to improve performance for large data sets or computationally intensive tasks — all without explicit thread management.

How to Create Parallel Streams

You can create a parallel stream in two ways:

Internally, the Java runtime uses the common ForkJoinPool to divide the stream elements into smaller chunks, processing them in parallel threads and combining results efficiently.

Benefits of Parallel Streams

Example: Sequential vs Parallel Processing

The following example demonstrates summing the squares of numbers sequentially and in parallel, measuring the time taken for each.

import java.util.List;
import java.util.stream.IntStream;

public class ParallelStreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = IntStream.rangeClosed(1, 10_000_000).boxed().toList();

        long startSeq = System.currentTimeMillis();
        long sumSeq = numbers.stream()
                            .mapToLong(n -> n * n)
                            .sum();
        long durationSeq = System.currentTimeMillis() - startSeq;
        System.out.println("Sequential sum: " + sumSeq + ", Time: " + durationSeq + " ms");

        long startPar = System.currentTimeMillis();
        long sumPar = numbers.parallelStream()
                            .mapToLong(n -> n * n)
                            .sum();
        long durationPar = System.currentTimeMillis() - startPar;
        System.out.println("Parallel sum: " + sumPar + ", Time: " + durationPar + " ms");
    }
}

In this example, the parallel stream often completes faster by dividing the workload, but the actual speedup depends on hardware, data size, and task complexity.

Summary

Parallel streams allow you to harness multicore CPUs for faster data processing with minimal coding effort. By understanding how to switch between sequential and parallel streams, you can optimize your applications for performance while maintaining readable, declarative code.

Index

9.2 When to Use Parallel Streams

Parallel streams can significantly speed up data processing by distributing work across multiple threads. However, not all tasks benefit from parallelization, so understanding when to use parallel streams is essential for writing efficient and correct code.

Guidelines for Using Parallel Streams

  1. Large Data Sets Parallel streams shine when processing large collections or data sources. Small or trivial data sets often incur more overhead in thread management than the performance gains they bring.

  2. CPU-Bound Operations If the operation on each element requires significant computation (e.g., complex calculations, transformations), parallel processing can reduce overall time by leveraging multiple cores.

  3. Independence of Elements Operations should be stateless and independent per element, meaning the processing of one item does not depend on or affect others. This avoids race conditions and synchronization bottlenecks.

  4. Cost Per Element The work done per element must be sufficiently expensive to justify the overhead of parallel execution. Simple, fast operations like incrementing integers or light filtering may not benefit.

When to Avoid Parallel Streams

Comparison to Other Concurrency Tools

While parallel streams abstract away thread management, traditional frameworks like ForkJoinPool and ExecutorService provide finer control over thread creation, task scheduling, and exception handling. Parallel streams are easier to use but less flexible.

Example: When Parallel Streams Help

import java.util.List;
import java.util.stream.IntStream;

public class ParallelStreamGuidelineExample {
    public static void main(String[] args) {
        List<Integer> largeNumbers = IntStream.rangeClosed(1, 1_000_000).boxed().toList();

        // CPU-intensive operation: compute sum of squares
        long sumParallel = largeNumbers.parallelStream()
                                       .mapToLong(n -> heavyComputation(n))
                                       .sum();

        System.out.println("Sum with parallel stream: " + sumParallel);
    }

    private static long heavyComputation(int n) {
        // Simulate expensive operation
        long result = 0;
        for (int i = 0; i < 1000; i++) {
            result += Math.sqrt(n * i);
        }
        return result;
    }
}

Example: When Not to Use Parallel Streams

import java.util.List;

public class SmallDatasetExample {
    public static void main(String[] args) {
        List<String> smallList = List.of("a", "b", "c");

        // Parallel overhead may outweigh benefits here
        long count = smallList.parallelStream()
                              .filter(s -> s.startsWith("a"))
                              .count();

        System.out.println("Count: " + count);
    }
}

Summary

Parallel streams are best suited for large, CPU-bound, independent data processing tasks with non-trivial per-element costs. For smaller, I/O-bound, or stateful operations, sequential streams or other concurrency mechanisms may be more appropriate. Understanding these trade-offs helps you choose the right tool for efficient and safe parallelism.

Index

9.3 Example: Parallel vs Sequential Performance

To understand the real benefits of parallel streams, it helps to compare their performance with sequential streams on a sufficiently large dataset. This example processes a large list of numbers, applying a moderately expensive transformation, and measures the execution time of both approaches.

Setup: Squaring Numbers with a Simulated Workload

We'll square numbers from 1 to 10 million, simulating some CPU work with a small delay, then compare sequential and parallel execution times.

import java.util.List;
import java.util.stream.IntStream;

public class ParallelVsSequentialPerformance {
    public static void main(String[] args) {
        List<Integer> numbers = IntStream.rangeClosed(1, 10_000_000).boxed().toList();

        // Sequential processing
        long startSeq = System.currentTimeMillis();
        long sumSeq = numbers.stream()
                            .mapToLong(ParallelVsSequentialPerformance::simulateWork)
                            .sum();
        long durationSeq = System.currentTimeMillis() - startSeq;
        System.out.println("Sequential sum: " + sumSeq + ", Time: " + durationSeq + " ms");

        // Parallel processing
        long startPar = System.currentTimeMillis();
        long sumPar = numbers.parallelStream()
                            .mapToLong(ParallelVsSequentialPerformance::simulateWork)
                            .sum();
        long durationPar = System.currentTimeMillis() - startPar;
        System.out.println("Parallel sum: " + sumPar + ", Time: " + durationPar + " ms");
    }

    private static long simulateWork(int n) {
        // Simulate moderate CPU workload
        double result = 0;
        for (int i = 0; i < 50; i++) {
            result += Math.sqrt(n + i);
        }
        return (long) result;
    }
}

Sample Output

Sequential sum: 670962544, Time: 2300 ms
Parallel sum: 670962544, Time: 800 ms

Analysis

When Parallelism May Hurt Performance

Summary

This example highlights how parallel streams can significantly reduce processing time for CPU-intensive, large-scale tasks. Benchmarking with your own data and operations is essential to decide if parallel streams are appropriate for your use case.

Index

9.4 Pitfalls and Thread Safety

While parallel streams simplify concurrent data processing, they also introduce risks related to thread safety and correctness. Understanding these pitfalls is crucial to avoid subtle bugs and unpredictable behavior.

Common Pitfalls with Parallel Streams

  1. Non-Associative Operations Operations like reduce() require the accumulator to be associative and stateless. Non-associative or stateful reductions can produce incorrect results or exceptions in parallel execution.

  2. Shared Mutable State Modifying shared objects (like collections or counters) inside stream operations—especially intermediate or terminal ones—can cause race conditions and data corruption when streams run in parallel.

  3. Side-Effects in Intermediate Operations Intermediate operations like map(), filter(), or peek() should be stateless and without side effects, as they might be invoked multiple times or out of order in parallel.

Example 1: Unsafe Shared Mutable State

This example demonstrates a common mistake—updating a non-thread-safe collection inside a parallel stream, leading to unpredictable output:

import java.util.ArrayList;
import java.util.List;

public class UnsafeParallelExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);
        List<Integer> results = new ArrayList<>();

        // Parallel stream modifying shared list — NOT thread-safe!
        numbers.parallelStream()
               .map(n -> n * 2)
               .forEach(results::add);

        System.out.println("Results: " + results);
    }
}

Problem: ArrayList is not thread-safe. Concurrent add() calls cause data races and may miss elements or throw exceptions.

Example 2: Safe Alternative Using Thread-Safe Collector

Use built-in thread-safe collectors like Collectors.toList() to safely gather results in parallel streams:

import java.util.List;
import java.util.stream.Collectors;

public class SafeParallelExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);

        List<Integer> results = numbers.parallelStream()
                                       .map(n -> n * 2)
                                       .collect(Collectors.toList()); // thread-safe

        System.out.println("Results: " + results);
    }
}

Here, collect() handles thread-safe accumulation internally, avoiding shared mutable state issues.

Additional Tips for Thread Safety

Summary

Parallel streams can introduce concurrency bugs when shared mutable state or non-associative operations are involved. Avoid side-effects in stream pipelines and rely on thread-safe collectors and stateless functions to ensure correct, predictable parallel processing.

Index