简体   繁体   中英

Parallel stream vs serial stream

Is it possible that a parallel stream could give a different result than a serial stream in Java 8? According to my information, a parallel stream is the same as a serial stream except divided into multiple substreams. It is a question of speed. All operations over the elements are done and the results of the substreams are combined at the end. In the end, the result of the operations should be the same for parallel and serial streams in my opinion. So my question is, is it possible that this code could give me a different result? And if it is possible, why does it happen?

int[] i = {1, 2, 5, 10, 9, 7, 25, 24, 26, 34, 21, 23, 23, 25, 27, 852, 654, 25, 58};
Double serial = Arrays.stream(i).filter(si -> {
    return si > 5;
}).mapToDouble(Double::new).map(NewClass::add).reduce(Math::atan2).getAsDouble();

Double parallel = Arrays.stream(i).filter(si -> {
    return si > 5;
}).parallel().mapToDouble(Double::new).map(NewClass::add).reduce(Math::atan2).getAsDouble();

System.out.println("serial: " + serial);
System.out.println("parallel: " + parallel);

public static double add(double i) {
    return i + 0.005;
}

and results are:

serial: 3.6971567726175894E-23

parallel: 0.779264049587662

The javadoc for reduce() says:

Performs a reduction on the elements of this stream, using an associative accumulation function, [...] The accumulator function must be an associative function.

The word "associative" is linked to this java doc:

An operator or function op is associative if the following holds:

  (a op b) op c == a op (b op c) 

The importance of this to parallel evaluation can be seen if we expand this to four terms:

  a op b op c op d == (a op b) op (c op d) 

So we can evaluate (a op b) in parallel with (c op d), and then invoke op on the results.

Examples of associative operations include numeric addition, min, and max, and string concatenation.

As @PaulBoddington mentioned in a comment, atan2 is not associative, and is therefore not valid for a reduction operation.


Unrelated

Your stream sequence is a bit off. You should filter after the parallel operation, the lambda can be shortened, and you shouldn't box the double:

double parallel = Arrays.stream(i)
                        .parallel()           // <-- before filter
                        .filter(si -> si > 5) // <-- shorter
                        .asDoubleStream()     // <-- not boxing
                        .reduce(Math::atan2)
                        .getAsDouble();

When you use reduce with a parallel stream, the operations are not done in a specific order.

Therefore if you want parallel streams to produce a predictable result, your reduce operation must have the same answer no matter what order things are done in.

For example, reducing using addition makes sense, because addition is associative. It doesn't matter which of these you do, the answer is 6 in both cases.

(1 + 2) + 3
1 + (2 + 3)

atan2 is not associative.

Math.atan2(Math.atan2(1, 2), 3) == 0.15333604941031637

whereas

Math.atan2(1, Math.atan2(2, 3)) == 1.0392451500584097

Your reduce method produces different results, if the elements are given in a different orders.

So if you use a parallel stream the original order is not garanteed.

If you use a different reduction method (eg (x,y) -> x+y) it works just fine.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM