Stream reduce()要求究竟需要什么?

时间:2017-07-12 09:55:31

标签: java parallel-processing java-8 java-stream reduce

在并行流上使用reduce()操作时,OCP考试书指出reduce()参数必须遵守某些原则。这些论点如下:

  1. 必须定义标识,以便对于流u中的所有元素,combiner.apply(identity,u)等于u。
  2. 累加器运算符op必须是关联的且无状态的,(a op b) op c等于a op (b op c)
  3. 组合器运算符还必须是关联的,无状态的并且与身份兼容,这样ut combiner.apply(u, accumulator.apply(identity, t))的所有内容都等于accumulator.apply(u,t)
  4. 考试书提供了两个例子来说明这些原则,请参阅下面的代码:

    关联的例子:

    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,但这与身份参数本身有什么关系呢?究竟什么是正确的身份呢?

    我想知道是否有人可以更多地了解这些原则。

    谢谢

4 个答案:

答案 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操作的结果将取决于:

  1. Stream内容
  2. 处理Stream
  3. 的线程数

关联性

让我们尝试一个非关联累加器/组合器的示例(基本上,我们通过改变线程数来减少序列中的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
  • 我不会使用Boyarsky&Selikoff OCP8的书本示例,因为流太小(1、2、3、4、5、6)并且对于ForkJoinPool产生(在我的机器上)相同的归约值3 1,2,3,4或5个线程。
  • 并行流的默认线程数是可用CPU核心数。这就是为什么您可能在每台机器上都无法获得相同的减少结果的原因。

身份

对于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