Parallel streams
An ordinary stream processes elements one at a time on a single processor core. A parallel stream splits the elements into parts and processes them at the same time in several threads. It is switched on with a single call, but it pays off far from always.
parallelStream
On a collection you call parallelStream() instead of stream(), and an existing stream is switched with the parallel() method. The rest of the chain stays the same:
void main() {
List<Integer> prices = List.of(120, 45, 300, 80);
int total = prices.parallelStream()
.mapToInt(p -> p)
.sum();
IO.println("Total: $" + total);
}Output:
Total: $545The result is the same as with an ordinary stream — only the way the work is done inside changes.
Who does the work
The parts of a parallel stream are processed by threads from the shared ForkJoinPool.commonPool(), plus the main thread itself. There are usually as many of them as the processor has cores. Let's print who handled each element:
void main() {
List.of(1, 2, 3, 4, 5, 6).parallelStream()
.forEach(n -> IO.println(n + " " + Thread.currentThread().getName()));
}Output (the order and names change from run to run):
6 ForkJoinPool.commonPool-worker-4
5 ForkJoinPool.commonPool-worker-5
1 ForkJoinPool.commonPool-worker-2
4 main
2 ForkJoinPool.commonPool-worker-1
3 ForkJoinPool.commonPool-worker-3Element order
In a parallel stream forEach runs the action in the order the parts finish — that is, in a random order. If the order matters, use forEachOrdered. And toList() and collect() keep the original order on their own:
void main() {
List<String> names = List.of("Anna", "Oleg", "Ira", "Peter");
names.parallelStream().forEachOrdered(n -> IO.print(n + " "));
IO.println("");
IO.println(names.parallelStream().map(String::toUpperCase).toList());
}Output:
Anna Oleg Ira Peter
[ANNA, OLEG, IRA, PETER]forEachOrdered makes the threads wait for each other, so part of the gain from parallelism is lost.
When it pays off
Parallelism speeds things up when there are many elements, each one takes noticeable computation, and each is processed independently of the others. For example, counting the primes below three million:
boolean isPrime(int n) {
if (n < 2) {
return false;
}
for (int d = 2; d * d <= n; d++) {
if (n % d == 0) {
return false;
}
}
return true;
}
void main() {
long start = System.currentTimeMillis();
long count = IntStream.range(0, 3_000_000).filter(this::isPrime).count();
IO.println("Sequential: " + count + " in " + (System.currentTimeMillis() - start) + " ms");
start = System.currentTimeMillis();
count = IntStream.range(0, 3_000_000).parallel().filter(this::isPrime).count();
IO.println("Parallel: " + count + " in " + (System.currentTimeMillis() - start) + " ms");
}Output (the time depends on the computer):
Sequential: 216816 in 1585 ms
Parallel: 216816 in 229 msAnd here is where parallelism hurts: small collections (splitting costs more than the work itself), waiting for the network or disk, sources that split poorly (LinkedList, Stream.iterate), and order-dependent operations — limit, findFirst, forEachOrdered. Do not turn on a parallel stream without measuring the time.
Common mistake
The most dangerous mistake is changing a shared variable or collection from a parallel stream. An ArrayList is not designed to be written to from several threads at once: some elements get lost, and sometimes the program crashes with an exception:
void main() {
List<Integer> result = new ArrayList<>();
IntStream.range(0, 10_000).parallel().forEach(n -> result.add(n));
IO.println(result.size());
}Output (changes from run to run):
6593Fewer than 10,000 elements, and a different number every time. Do not modify outside objects from a lambda; let the stream collect the result itself: IntStream.range(0, 10_000).parallel().boxed().toList() always returns all 10,000 elements in order.