You are currently viewing Parallel Stream Processing: Performance and Risks

Parallel Stream Processing: Performance and Risks

This entry is part 9 of 9 in the series Modern Java Features (Java 8+)

Introduction

Parallel streams offer an attractive promise: improved performance by leveraging multiple CPU cores with minimal code changes. However, this promise only holds under strict conditions. Parallel streams introduce a different execution model, and code that works perfectly in a sequential stream may fail, behave unpredictably, or even run slower when executed in parallel.

This article explains:

  • how parallel streams actually work,
  • the key restrictions they impose,
  • and when parallel streams should—or should not—be used in production code.

Understanding these limitations is essential before enabling parallelism.

“Parallel streams optimize throughput, not correctness. Correctness is your responsibility.”

1. What Parallel Streams Actually Do

If you are new to the Stream API, read our article Java Stream API: What it is and what it’s not, for a detailed explanation.

A parallel stream divides its source into multiple chunks and processes those chunks concurrently using the Fork/Join common pool.

The simplest way to see this difference is to observe thread usage.

1.1. Sequential stream

numbers.stream()
       .forEach(n ->
           System.out.println(Thread.currentThread().getName() + " -> " + n)
       );

Output (typical):

main -> 1
main -> 2
main -> 3
main -> 4

All elements are processed:

  • on a single thread,
  • sequentially,
  • in encounter order.

1.2. Parallel stream

numbers.parallelStream()
       .forEach(n ->
           System.out.println(Thread.currentThread().getName() + " -> " + n)
       );

Output (typical):

ForkJoinPool.commonPool-worker-2 -> 3
ForkJoinPool.commonPool-worker-5 -> 1
ForkJoinPool.commonPool-worker-1 -> 4
ForkJoinPool.commonPool-worker-3 -> 2

Here:

  • multiple threads are involved,
  • execution order is not guaranteed,
  • operations may overlap in time.

Parallel streams do not change what your pipeline does—only how and when it runs.

2. Key Restrictions and Challenges

Parallel execution imposes strict rules. Violating them leads to incorrect results or performance degradation.

2.1. Stateless and Non-Interfering Operations Are Mandatory

Parallel streams require operations to be:

  • stateless (no shared mutable state),
  • non-interfering (do not modify the stream source).

✖️ Incorrect: shared mutable state

int[] sum = new int[1];//shared mutable state

numbers.parallelStream()
       .forEach(n -> sum[0] += n);//race condition

System.out.println(sum[0]); // unpredictable result

In the example above, multiple threads update sum[0] concurrently, causing race conditions. On small datasets, you may not observe any difference. However, on large datasets, the result is unpredictable.

☑️ Correct approach

You should avoid using a shared mutable state:

int sum = numbers.parallelStream()
                 .mapToInt(Integer::intValue)
                 .sum();

If an operation depends on shared state, parallel streams are unsafe.

2.2. Side Effects and Why They Break Parallel Streams

Side effects include:

  • mutating external collections,
  • updating fields,
  • writing logs or files.

✖️ Side effects with non-deterministic behavior

List<Integer> result = new ArrayList<>();

numbers.parallelStream()
       .forEach(result::add);//concurrent writes

System.out.println(result.size()); // may be incorrect

ArrayList is not thread-safe. Concurrent writes corrupt internal state.

☑️ Correct alternative

List<Integer> result = numbers.parallelStream().toList();

Side effects introduce race conditions and destroy determinism.

2.3. Ordering Constraints and Their Performance Cost

Some stream sources (like List) are ordered. Parallel streams may ignore this order unless explicitly preserved.

✖️ Order not guaranteed

numbers.parallelStream()
       .forEach(System.out::println);

Output order is unpredictable.

☑️ Order preserved (with a cost)

numbers.parallelStream()
       .forEachOrdered(System.out::println);

Preserving order:

  • reduces parallelism,
  • introduces synchronization,
  • often negates performance benefits.

Order preservation trades performance for determinism.

2.4. Blocking Operations and I/O Are a Poor Fit

Parallel streams use the ForkJoin common pool, which is optimized for CPU-bound tasks. Blocking operations can exhaust this pool.

✖️ Blocking I/O in a parallel stream

urls.parallelStream()
    .map(this::fetchRemoteData)
    .toList();

If fetchRemoteData blocks:

  • worker threads are stalled,
  • other parallel tasks are delayed,
  • overall application performance suffers.

☑️ Better alternative

Use an explicit executor (CompletableFuture, ExecutorService) for I/O-bound work.

2.5. Parallel Streams Are Not Always Faster

Parallelism introduces overhead:

  • task splitting,
  • thread coordination,
  • result merging.

✖️ Small workload

IntStream.range(1, 100)
         .parallel()
         .map(n -> n * 2)
         .sum();

For small datasets or trivial operations, parallel execution is often slower than sequential.

Parallelism has a cost; it must be amortized.

Always measure before parallelizing.

2.6. ForkJoinPool Limitations

Parallel streams use the shared ForkJoinPool.commonPool() by default.

This means:

  • all parallel streams share the same pool,
  • blocking tasks affect unrelated code,
  • pool size is limited (usually number of CPU cores).

✖️ Competing parallel streams

streamA.parallel().forEach(this::cpuTask);
streamB.parallel().forEach(this::anotherCpuTask);

Both streams compete for the same threads, reducing throughput.

Parallel streams provide no control over:

  • pool size,
  • thread priority,
  • task isolation.

3. When to Use Parallel Streams (And When Not To)

☑️ Use parallel streams when:

  • the dataset is large,
  • operations are CPU-intensive,
  • operations are stateless and associative,
  • ordering is irrelevant.

✖️ Avoid parallel streams when:

  • operations have side effects,
  • tasks are I/O-bound,
  • strict ordering is required,
  • fine-grained control over threads is needed.

4. Safer Alternatives to Parallel Streams

In many cases, explicit concurrency is safer and clearer:

  • CompletableFuture for asynchronous workflows,
  • ExecutorService for controlled parallelism,
  • reactive frameworks for I/O-heavy pipelines.

These alternatives offer:

  • explicit thread management,
  • better error handling,
  • predictable performance characteristics.

Parallel streams trade control for convenience.

Learn more about Stream operations and pipelines by visiting our dedicated article: Stream operations and pipelines.

Conclusion

Parallel streams are powerful but constrained. They rely on stateless, non-interfering operations and work best for large, CPU-bound workloads. Used incorrectly, they introduce subtle bugs, race conditions, and performance regressions.

Before using parallel streams, ask:

  • Is the operation free of side effects?
  • Is ordering irrelevant?
  • Is the workload CPU-bound?
  • Does parallel overhead justify the cost?

“Parallel streams are a precision tool, not a default choice.”

You can find the complete code of this article here on GitHub.

Modern Java Features (Java 8+)

Bi-Argument Functional Interfaces in the Stream API

Noel Kamphoa

Experienced software engineer with expertise in Telecom, Payroll, and Banking. Now Senior Software Engineer at Societe Generale Paris.