Parallel Streams in Java
A parallel stream in Java splits its elements into parts and processes the parts at the same time on several CPU cores. You create one with collection.parallelStream() or by calling .parallel() on a stream. Behind the scenes, it uses the JDK's shared ForkJoinPool.commonPool(). Parallel streams can make big, CPU-heavy calculations much faster, but on small data, or with the wrong operations, they are slower and can even give wrong results. In this lesson, you will learn how they work and how to decide whether to use them.
Syntax
list.parallelStream()... // from a collection
stream.parallel()... // switch an existing stream
stream.sequential()... // switch back
stream.isParallel() // checkHow a Parallel Stream Works
- Split: the source is divided into chunks using a
Spliterator. - Process: each chunk goes through the pipeline on a different worker thread of the common pool.
- Combine: the partial results are merged into the final result.
The common pool has, by default, one fewer worker than the number of CPU cores, and the calling thread helps too.
Example: A Parallel Sum
Let's see an example that sums the square roots of 20 million numbers both ways. The times depend on the computer, so the output is a sample from a 10-core laptop.
import java.util.stream.LongStream;
public class ParallelSum {
static double work(boolean parallel) {
LongStream s = LongStream.rangeClosed(1, 20_000_000);
if (parallel) {
s = s.parallel();
}
return s.mapToDouble(Math::sqrt).sum();
}
public static void main(String[] args) {
work(false);
work(true);
long t = System.nanoTime();
double a = work(false);
long seq = (System.nanoTime() - t) / 1_000_000;
t = System.nanoTime();
double b = work(true);
long par = (System.nanoTime() - t) / 1_000_000;
System.out.printf("same result: %b%n", Math.abs(a - b) < 1e-3 * a);
System.out.println("sequential: " + seq + " ms");
System.out.println("parallel: " + par + " ms");
System.out.println("cores: "
+ Runtime.getRuntime().availableProcessors());
}
}same result: true
sequential: 37 ms
parallel: 5 ms
cores: 10Explanation: Each method is run once first, to let the JVM warm up. The parallel version spreads the 20 million calculations over all cores and finishes several times faster. The sums are compared with a small tolerance, because adding double values in a different order can change the last few digits.
When Parallel Streams Help and When They Hurt
| Helps | Hurts |
|---|---|
| Many elements (tens of thousands or more) | Few elements |
| Expensive work per element (math, parsing) | Cheap work per element |
Sources that split well: ArrayList, arrays, IntStream.range | Sources that split badly: LinkedList, Stream.iterate, I/O |
| Stateless, independent operations | sorted, limit, findFirst, forEachOrdered on ordered data |
| CPU-bound work | Waiting work (network, database, files) |
Example: A Parallel Stream Giving the Wrong Answer
Shared mutable state breaks parallel streams. Adding to an ArrayList from many threads loses elements; the exact numbers change on every run:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
public class Unsafe {
public static void main(String[] args) {
List<Integer> unsafe = new ArrayList<>();
try {
IntStream.range(0, 100_000).parallel().forEach(unsafe::add);
System.out.println("unsafe size: " + unsafe.size());
} catch (RuntimeException e) {
System.out.println("unsafe failed: "
+ e.getClass().getSimpleName());
}
List<Integer> safe = IntStream.range(0, 100_000).parallel()
.boxed().toList();
System.out.println("safe size: " + safe.size());
}
}unsafe size: 14396
safe size: 100000Explanation: Many threads called add on the same ArrayList at the same time, so updates collided and elements were lost (sometimes the list even throws). toList() gives each thread its own partial list and merges them safely, so all 100,000 elements arrive.
Common Mistakes
Making every stream parallel "for speed". Most collections in everyday code are too small to benefit.
Doing blocking I/O in a parallel stream. It ties up the common pool that other parts of the program share.
Using forEach with shared state. Use collectors, which merge results safely.
Expecting forEach to keep the order. Use forEachOrdered if order matters.
Interview Corner
When should you use parallel streams?
When the data set is large, the work per element is CPU-intensive and independent, the source splits efficiently (arrays, ArrayList, ranges), and the operations are stateless and free of side effects. Measure before and after. Avoid them for small collections, I/O-bound work, sources such as LinkedList or Stream.iterate, order-dependent operations, and any code that mutates shared state. They run on the shared ForkJoinPool.commonPool().
Practice
- Easy: Change the size in
ParallelSumto 1,000 and run it several times. Which is faster now, and why? - Harder: Count the prime numbers below 2,000,000 with a sequential and a parallel
IntStream, using a simpleisPrimemethod, and compare the times.
Key Takeaways
Was this tutorial helpful?