Functional Programming & Streams
Parallel Streams
One word —
parallel() — splits a stream across the common ForkJoinPool. It is effortless and frequently a mistake: parallelism pays only for large, splittable sources with CPU-heavy, independent stages (EJ 48).- Parallel wins need: many elements × costly-per-element × splittable source × no shared state
- Best sources: arrays,
ArrayList,IntStream.range— cheap to split;iterateandlimitare poison - All pipeline functions must be thread-safe and side-effect-free
- Ordering constraints (
findFirst,forEachOrdered) sacrifice speedup;findAnydoesn't - Everything shares the common FJ pool — one blocked parallel stream starves the JVM's others
- Measure before and after — never assume parallel is faster
// GOOD: huge range, pure math, splittable source, associative reduction
long primes = LongStream.range(2, 10_000_000)
.parallel()
.filter(MathUtils::isPrime)
.count();
// BAD (from EJ Item 48): Stream.iterate can't split, limit fights parallelism —
// this version doesn't just fail to speed up; it can run effectively forever:
Stream.iterate(TWO, BigInteger::nextProbablePrime)
.parallel()
.limit(20)
.forEach(System.out::println);Under the hood: the source's Spliterator recursively halves the data, fork/join workers process chunks, and results merge via your combiner/collector. The model (from Fork/Join) assumes non-blocking, CPU-bound work. I/O in a parallel stream occupies the shared common pool — other parallel streams and CompletableFuture defaults stall with it.
Rules of thumb from the performance books: below ~10,000 cheap elements, splitting overhead eats the gain (the old N×Q heuristic — element count × per-element cost should exceed ~100k "units"). Merge cost matters too: groupingBy into maps merges expensively in parallel — groupingByConcurrent exists for that. And benchmark with JMH, not wall-clock printlns.