Parallel Streams and Stream Best Practices

A parallel stream splits the work across threads. It helps less often than people expect, and it breaks silently when the rules are ignored.

Creating a parallel stream

long count = notes.parallelStream().filter(Note::published).count();
long same = notes.stream().parallel().filter(Note::published).count();

The pipeline is split into chunks processed by the common ForkJoin pool, and the partial results are merged. By default that pool has one thread per available processor, minus one.

When it helps

FavourableUnfavourable
Many elements, typically tens of thousandsA small collection
Expensive work per elementTrivial work per element
A source that splits well: array, ArrayList, IntStream.rangeLinkedList, Files.lines, iterators
Stateless, independent operationsShared mutable state
A cheap combinerExpensive merging
Nothing else competing for the poolBlocking I/O inside the pipeline
Splitting, scheduling and merging all cost time. For a list of a few hundred elements a parallel stream is reliably slower than a sequential one. Measure; do not assume.

The rules a parallel stream requires

1. No shared mutable state

// Broken: ArrayList is not thread safe, results are lost or corrupted
List<String> titles = new ArrayList<>();
notes.parallelStream().forEach(note -> titles.add(note.title()));

// Correct: collect handles the merging safely
List<String> titles = notes.parallelStream().map(Note::title).toList();

2. The accumulator must be associative

// Associative: grouping does not change the answer
int sum = numbers.parallelStream().reduce(0, Integer::sum);

// Not associative: subtraction gives different results per split
int wrong = numbers.parallelStream().reduce(0, (a, b) -> a - b);

3. The identity must be neutral

// Wrong: each parallel chunk starts from 10, so 10 is added several times
int total = numbers.parallelStream().reduce(10, Integer::sum);

This one is insidious: the sequential version gives the expected answer and only the parallel version is wrong.

4. Do not modify the source

List<String> names = new ArrayList<>(List.of("a", "b"));
// names.parallelStream().forEach(name -> names.add(name));   // undefined behaviour

Ordering

List<Integer> values = IntStream.rangeClosed(1, 10).boxed().toList();

values.parallelStream().forEach(System.out::print);         // order unpredictable
values.parallelStream().forEachOrdered(System.out::print);  // 12345678910

List<Integer> collected = values.parallelStream().map(n -> n * 2).toList();
// collected is always in order: collect preserves encounter order

Collecting preserves encounter order; forEach does not. findAny may return any matching element, while findFirst forces the first in encounter order and therefore costs more in parallel.

The common pool is shared

// A blocking call inside a parallel stream starves every other user of the pool
urls.parallelStream().map(this::httpGet).toList();     // avoid
// Run it in a pool you control instead
ForkJoinPool pool = new ForkJoinPool(8);
try {
    List<String> results = pool.submit(
            () -> urls.parallelStream().map(this::httpGet).toList()).get();
} finally {
    pool.shutdown();
}

Every parallel stream in the JVM shares one common pool by default. Blocking work inside it delays everything else. For I/O bound work, an executor or virtual threads are a far better fit than parallel streams, which are designed for CPU bound work.

Measuring rather than guessing

long start = System.nanoTime();
long result = data.stream().filter(this::expensiveCheck).count();
long sequentialMs = (System.nanoTime() - start) / 1_000_000;

start = System.nanoTime();
long parallelResult = data.parallelStream().filter(this::expensiveCheck).count();
long parallelMs = (System.nanoTime() - start) / 1_000_000;

This is a rough comparison, not a benchmark: JIT warm up and garbage collection both distort a single measurement. Treat the numbers as an indication, and use a proper benchmarking harness for decisions that matter.

Stream best practices

  • Prefer a loop when it is clearer. A stream is not automatically better.
  • Keep pipelines side effect free. Every lambda should depend only on its input.
  • Filter early, sort late. Reduce the data before the expensive stages.
  • Use primitive streams for numeric work to avoid boxing.
  • Extract long lambdas into named methods and use method references.
  • Do not reuse a stream. Create a new one from the source.
  • Close I/O streams such as Files.lines with try with resources.
  • Do not nest deeply. A stream inside a stream inside a collector is usually a sign to extract a method.
  • Go parallel only after measuring, and only for large, CPU bound, splittable work.

Closing an I/O backed stream

try (Stream<String> lines = Files.lines(path)) {
    long blank = lines.filter(String::isBlank).count();
}   // the file handle is released here

Common mistakes

  • Calling parallelStream on a small collection and losing performance.
  • Accumulating into a plain ArrayList from a parallel forEach.
  • Using a non neutral identity, so only the parallel result is wrong.
  • Doing blocking I/O inside a parallel stream and starving the common pool.
  • Expecting forEach to preserve order.
  • Using a LinkedList as a parallel source, which splits poorly.

Practice

  1. Explain why reduce(10, Integer::sum) gives a different answer in parallel.
  2. Rewrite a parallel forEach that fills a list so that it is thread safe.
  3. Compare sequential and parallel timings for a cheap operation over 1000 elements, and explain the result.
  4. Why is findAny cheaper than findFirst in a parallel stream?
  5. List three properties a source needs for parallelism to pay off.

Conclusion

Parallel streams are for large, CPU bound, cleanly splittable work with no shared state. Keep operations associative and side effect free, avoid blocking the common pool, and measure before and after rather than trusting intuition.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.