Be Careful With Java Parallel Streams
By: Semyon Kirekov
Tags: Functional Programming, Multithreading, Java, Stream, Parallel Computing

Java Stream API is awesome. That’s one of the greatest enhancements since Java 8. It allows us to build complex queries over collections in a declarative way. Also, Stream API provides an easy approach for parallel execution. Just add the parallel() statement or construct the stream with parallelStream() function. But there is a caveat that you should be aware of if you don’t want to handle unexpected bugs.

Suppose we need to multiply the given set of numbers, the Stream API can come in handy.

Stream reducing

The result is 524490120 . Assume that this number should also be multiplied by 5. No problem. We can just replace the identity of reduction.

Stream reducing with a different identity

The answer is 2622450600 . Everything works as expected.

What if the stream contained hundreds or thousands of numbers? The sequential execution might be too slow. Let’s make the stream parallel and test the results for equality.

Parallel stream reducing with a different identity

Surprisingly the result on my machine is 40975790625000 . That’s definitely doesn’t match with 2622450600 .

What’s going on here? Is there a bug with parallel streams in Java? Actually, everything works correctly. Let’s go deeper and see how exactly parallel streaming is working.

ForkJoinPool

Java Streams use ForkJoinPool to launch parallel executions. Describing the details of this executor is out of scope for this article. But the main idea is that the task splits into multiple chunks recursively and then each of them can be computed independently.

So, here is the diagram for Stream.reduce sequential execution.

Sequential reducing

As you can see, the algorithm is pretty straight forward. Suppose, we want to parallelize the execution. To make it easier let’s assume that the task splits only into two chunks.

Parallel reducing

Each chunk has been multiplied by 5. Because that’s the way parallel stream works. It applies the given identity for each determined chunk.

How can we fix that? All we need to do is to place 5 outside of the reduce function.

Bugs-free parallel stream reducing

Multiplying by 1 does not affect the result. So, no matter how many chunks there will be.

According to the given example, I can give you some advice for designing your Stream API cases.

Reduce should splittable

If you are not sure whether the stream is sequential or not (e.g., it comes as function parameter), the identity of reduce function should not affect the result of individual chunks. That means, that summing function has to accept 0 while multiplying function 1 .

Perhaps you don’t need a parallel stream

Not all stream operation should be parallelized. For example, map , flatMap , and filter are good candidates because they are stateless. On the other hand, sorted , distinct , and limit may not show good performance enhancement.

More than that, the effectiveness of parallelization strongly depends on the source of the stream. An ArrayList , an array, or the IntStream.range support random access, which means that they can be split up with ease. But LinkedList takes O(n) time to decompose. Also Stream.iterate and BufferedReader are not a good choice either because they both have an unknown length at the beginning, so it’s hard to estimate when to split sources.

Write unit tests

We’ve seen that every parallel stream is not only a potential performance benefit but a spot for bugs. So, every time you replace a sequential stream with a parallel one, make sure that the functionality has not been broken.

Conclusion

As we can see, Stream API is not so obvious as it seems to be. I want to recommend a book that perfectly describes not only Stream API and its caveats, but Java 8 lambdas at all. It’s “Java 8 Lambdas: Pragmatic Functional Programming”. If you have any questions or suggestions, please write your comments down below. Thanks for reading!