在并行流上使用reduce()
操作时,OCP考试书指出reduce()
参数必须遵守某些原则。这些论点如下:
(a op b) op c
等于a op (b op c)
。u
和t
combiner.apply(u, accumulator.apply(identity, t))
的所有内容都等于accumulator.apply(u,t)
。考试书提供了两个例子来说明这些原则,请参阅下面的代码:
关联的例子:
System.out.println(Arrays,asList(1,2,3,4,5,6))
.parallelStream()
.reduce(0,(a,b) -> (a-b))); //NOT AN ASSOCIATIVE ACCUMULATOR
OCP书中有关于此的内容:
可以输出-21,3或其他值作为累加器功能 违反了相关性。
身份要求的示例:
System.out.println(Arrays.asList("w","o","l","f"))
.parallelStream()
.reduce("X", String::concat));
OCP书中有关于此的内容:
如果我们使用不是的身份参数,您可以看到其他问题 真正的身份价值。它可以输出XwXoXlXf。作为一部分 并行处理,将标识应用于多个元素中 流,导致非常意外的数据。
我不理解这些例子。使用累加器示例,累加器以0 -1 = -1然后-1 -2开始,其中= -3然后-6等等一直到-21。我理解,因为生成的arraylist不同步,结果可能是不可预测的,因为竞争条件等的可能性,但为什么累加器关联? Woulden&#t; t (a+b)
也导致不可预测的结果?我真的没有看到示例中使用的累加器有什么问题,以及为什么它没有关联,但是我仍然不能完全理解关联原则是什么。
我也不了解身份的例子。据我所知,如果4个独立的线程同时开始与身份一起累积,那么结果确实可能是XwXoXlXf,但这与身份参数本身有什么关系呢?究竟什么是正确的身份呢?
我想知道是否有人可以更多地了解这些原则。
谢谢
答案 0 :(得分:4)
为什么累加器不是关联的?
它不是关联的,因为减法运算的顺序决定了最终的结果。
如果您运行序列Stream
,您将获得预期的结果:
0 - 1 - 2 - 3 - 4 - 5 - 6 = -21
另一方面,对于并行Stream
,工作被拆分为多个线程。例如,如果在6个线程上并行执行reduce
,然后组合中间结果,则可以得到不同的结果:
0 - 1 0 - 2 0 - 3 0 - 4 0 - 5 0 - 6
-1 -2 -3 -4 -5 -6
-1 - (-2) -3 - (-4) -5 - (-6)
1 1 1
1 - 1
0 - 1
-1
或者,简而言之:
(1 - 2) - 3 = -4
1 - (2 - 3) = 2
因此减法不是关联的。
另一方面,a+b
不会导致同样的问题,因为加法是一个关联运算符(即(a+b)+c == a+(b+c)
)。
标识示例的问题是,当在多个线程上并行执行reduce时,“X”将附加到每个中间结果的开头。
究竟什么样的身份才能使用?
如果您将标识值更改为""
:
System.out.println(Arrays.asList("w","o","l","f"))
.parallelStream()
.reduce("", String::concat));
你会得到“狼”而不是“XwXoXlXf”。
答案 1 :(得分:4)
我举两个例子。首先是身份被破坏的地方:
int result = Stream.of(1, 2, 3, 4, 5, 6)
.parallel()
.reduce(10, (a, b) -> a + b);
System.out.println(result); // 81 on my run
基本上你违反了这条规则:The identity value must be an identity for the accumulator function. This means that for all u, accumulator(identity, u) is equal to u
。
或者为了更简单,让我们看看该规则是否适用于我们的Stream中的一些随机数据:
Integer identity = 10;
BinaryOperator<Integer> combiner = (x, y) -> x + y;
boolean identityRespected = combiner.apply(identity, 1) == 1;
System.out.println(identityRespected); // prints false
第二个例子:
/**
* count letters, adding a bit more all the time
*/
private static int howMany(List<String> tokens) {
return tokens.stream()
.parallel()
.reduce(0, // identity
(i, s) -> { // accumulator
return s.length() + i;
}, (left, right) -> { // combiner
return left + right + left; // notice the extra left here
});
}
你用以下方式调用它:
List<String> left = Arrays.asList("aa", "bbb", "cccc", "ddddd", "eeeeee");
List<String> right = Arrays.asList("aa", "bbb", "cccc", "ddddd", "eeeeee", "");
System.out.println(howMany(left)); // 38 on my run
System.out.println(howMany(right)); // 50 on my run
基本上你违反了这条规则:Additionally, the combiner function must be compatible with the accumulator function
或代码:
// this must hold!
// combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
Integer identity = 0;
String t = "aa";
Integer u = 3; // "bbb"
BiFunction<Integer, String, Integer> accumulator = (Integer i, String s) -> i + s.length();
BinaryOperator<Integer> combiner = (left, right) -> left + right + left;
int first = accumulator.apply(identity, t); // 2
int second = combiner.apply(u, first); // 3 + 2 + 3 = 8
Integer shouldBe8 = accumulator.apply(u, t);
System.out.println(shouldBe8 == second); // false
答案 2 :(得分:2)
虽然问题已经得到回答和接受,但我认为可以用更简单,更实用的方式来回答问题。
如果您没有有效的identity
和关联的累加器/合并器,则reduce
操作的结果将取决于:
Stream
内容Stream
让我们尝试一个非关联累加器/组合器的示例(基本上,我们通过改变线程数来减少序列中的50个数字的列表)
System.out.println("sequential: reduce="+
IntStream.rangeClosed(1, 50).boxed()
.reduce(
0,
(a,b)->a-b,
(a,b)->a-b));
for (int n=1; n<6; n++) {
ForkJoinPool pool = new ForkJoinPool(n);
final int finalN = n;
try {
pool.submit(()->{
System.out.println(finalN+" threads : reduce="+
IntStream.rangeClosed(1, 50).boxed()
.parallel()
.reduce(
0,
(a,b)->a-b,
(a,b)->a-b));
}).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
pool.shutdown();
}
}
这将显示以下结果(Oracle JDK 10.0.1):
sequential: reduce=-1275
1 threads : reduce=325
2 threads : reduce=-175
3 threads : reduce=-25
4 threads : reduce=75
5 threads : reduce=-25
这表明结果取决于reduce计算中涉及的线程数。
注意:
Stream
内容和相同数量的线程在多次运行时总是导致相同的减少值。我想这是因为并行流使用确定性Spliterator
。ForkJoinPool
产生(在我的机器上)相同的归约值3 1,2,3,4或5个线程。对于identity
,就像Eran在“ XwXoXlXf”示例中所写的那样,它有4个线程,每个线程将以identity
作为String
前缀的形式开始。但是请注意:尽管OCP书籍建议""
和0
是有效的identity
,但它取决于累加器/合并器功能。例如:
0
是累加器identity
的有效(a,b)->a+b
(因为a+0=a
)1
是累加器identity
的有效(a,b)->a*b
(因为a*1=a
,但0
无效,因为a*0=0
!)答案 3 :(得分:2)
顺序流的归约如下:顺序地将归约函数应用于流的每一对元素,期望在每一步接收一个元素与流的其他元素相同的类型。在下一步再次应用相同的功能,依此类推。
a b c d e
│ │ │ │ │
└─┬─┘ │ │ │
a+b │ │ │ a+b=sum1
│ │ │ │
└──┬──┘ │ │
sum1+c │ │ sum1+c=sum2
│ │ │
└──┬───┘ │
sum2+d │ sum2+d=sum3
│ │
└──┬────┘
sum3+e sum3+e=total;
减少并行流具有相同的期望,但不能保证下一步应该捕获哪对元素(或它们来自先前步骤的总和)。因此,结果可能会有所不同。
a b c d e
│ │ │ │ │
└─┬─┘ └─┬─┘ │
a+b c+d │ a+b=sum1 c+d=sum2
or:
│ │ │ │
└─┬─┘ └─┬─┘
b+c d+e b+c=sum1 d+e=sum2
│ │ │
└───┬───┘ │
sum1+sum2 │ sum..+sum..=sum..
or:
│ │ │ │
└──┬──┘ └──┬──┘
a+sum1 sum2+e sum..+sum..=sum..
另见:Generate all possible string combinations by replacing the hidden “#” number sign