When are parallel streams worth using in Java, and what makes them pay off?
A parallel stream is worth it when the work is CPU-bound, there is enough of it to amortise the overhead, the pipeline has enough stages to be split, and the source is large and easily splittable.
CPU-bound is the key qualifier. A parallel stream splits the work across the common ForkJoinPool, so it helps when there are spare cores and the task is arithmetic or computation. It does not help with IO-bound work — reading from a database, calling a remote service — because the threads spend their time blocked, and the parallelism you need there comes from an asynchronous client or a thread pool sized for waiting, not from the stream API.
The other three conditions matter. The work has to be big enough that the cost of splitting it is negligible; for a list of a hundred elements the overhead dominates. There need to be enough operations for the work to be divided into meaningful chunks. And the source has to be splittable — a List and an array split cleanly, whereas an Iterator or a linked list does not, and splitting a poorly splittable source can cost more than the parallelism gains.
One more condition that people forget: the pipeline must be free of shared mutable state. If your lambda writes to a shared counter, the parallel version produces a wrong answer rather than a wrong performance figure, and you will not notice from the output alone.
The rule of thumb most teams use is to write the sequential version first, and only reach for parallel when a measurement shows the sequential version is the bottleneck. parallelStream() is a one-word change, so there is no cost to trying it — but there is a cost to shipping a subtly wrong answer, so measure before you keep it.
Here is the same thing as runnable code:
public class Main {
public static void main(String[] args) {
int n = 5_000_000;
long start = System.nanoTime();
long sequential = java.util.stream.IntStream.range(0, n).map(i -> i * 2).sum();
long seqMs = (System.nanoTime() - start) / 1_000_000;
start = System.nanoTime();
long parallel = java.util.stream.IntStream.range(0, n).parallel().map(i -> i * 2).sum();
long parMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("sequential : " + sequential + " in " + seqMs + " ms");
System.out.println("parallel : " + parallel + " in " + parMs + " ms");
System.out.println("same result: " + (sequential == parallel));
}
}