在使用并行流时,reduce的行为异常,但在Java8U5中,对于顺序流来说效果很好

在使用并行流时,reduce的行为异常,但在Java8U5中,对于顺序流来说效果很好,java,concurrency,parallel-processing,java-8,Java,Concurrency,Parallel Processing,Java 8,代码尝试计算几个字符串的总长度 此代码仅在以下情况下打印19 1.我使用顺序流(通过删除“parallel()”函数调用) 或 2.我使用整数而不是Foo,Foo只是int的包装器 否则,控制台将打印20或36。为了调试这个问题,我添加了代码“checkselfreduction”,它确实改变了输出:“equal”总是被打印两次。控制台有时打印8,有时打印10 我的理解是reduce()是并行foldr/foldl的Java实现。reduce()的第三个参数combiner用于合并并行执行

代码尝试计算几个字符串的总长度

此代码仅在以下情况下打印19
1.我使用顺序流(通过删除“parallel()”函数调用)

2.我使用整数而不是Foo,Foo只是int的包装器

否则,控制台将打印20或36。为了调试这个问题,我添加了代码“checkselfreduction”,它确实改变了输出:“equal”总是被打印两次。控制台有时打印8,有时打印10

我的理解是reduce()是并行foldr/foldl的Java实现。reduce()的第三个参数combiner用于合并并行执行reduce的结果。是这样吗?如果是这样,为什么减排的结果需要与自身相结合?此外,如何修复此代码,使其提供正确的输出并仍然并行运行

编辑:
请忽略我没有使用方法引用来简化代码这一事实,因为我的最终目标是通过向Foo添加更多字段来压缩代码。

我认为问题在于“identity”
Foo
被重用得太多了

这里有一个修改,每个
Foo
都有自己的ID号,以便我们可以跟踪它:

class Foo{
    int len;
}
public class Main {
    public static void main(String[] args) throws Exception{
    System.out.println(Stream.of("alpha", "beta", "gamma", "delta").parallel().reduce(
            new Foo(),
            (f, s) -> { f.len += s.length(); return f; },
            (f1, f2) -> {
                Foo f = new Foo();
                /* check self-reduction
                if (f1 == f2) { 
                    System.out.println("equal");
                    f.len = f1.len;
                    return f;
                }
                */
                f.len = f1.len + f2.len;
                return f;
            }
    ).len);
}
我得到的结果是:

class Foo {
    private static int currId = 0;
    private static Object lock = new Object();
    int id;
    int len;
    public Foo() {
        synchronized(lock) {
            id = currId++;
        }
    }    
}

public class Main {
    public static void main(String[] args) throws Exception{
    System.out.println(Stream.of("alpha", "beta", "gamma", "delta").parallel().reduce(
            new Foo(),
            (f, s) -> {
                System.out.println("Adding to #" + f.id + ": " +
                     f.len + " + " + s.length() + " => " + (f.len+s.length())); 
                f.len += s.length(); return f; },
            (f1, f2) -> {
                Foo f = new Foo();
                f.len = f1.len + f2.len;
                System.out.println("Creating new #" + f.id + " from #" + f1.id + " and #" + f2.id + ": " +
                    f1.len + " + " + f2.len + " => " + (f1.len+f2.len));
                return f;
            }
    ).len);
}

不是每次都是一致的。我注意到的是,每次你说
f.leng+=s.length()
,它都会添加到相同的
Foo
,这意味着第一个
new Foo()
只执行一次,并且长度会不断添加到其中,因此相同输入字符串的长度会被多次计数。因为显然有多个并行线程同时访问它,所以上面的结果有点奇怪,并且在不同的运行中会发生变化。

您的代码被严重破坏。您使用的是一个reducer函数,它没有满足累加器/组合器函数是关联的、无状态的和无干扰的要求。一个可变的Foo不是一个简化的标识。所有这些在并行执行时都可能导致错误的结果

你也让事情变得比你需要的困难得多!试试这个:

Adding to #0: 0 + 5 => 5
Adding to #0: 0 + 4 => 4
Adding to #0: 5 + 5 => 10
Adding to #0: 9 + 5 => 14
Creating new #2 from #0 and #0: 19 + 19 => 38
Creating new #1 from #0 and #0: 14 + 14 => 28
Creating new #3 from #2 and #1: 38 + 28 => 66
66


此外,您尝试使用
reduce
来减少过多的值(这就是为什么它与
Integer
一起工作),但是您尝试使用可变状态容器来获得缩减结果。如果您想简化为可变状态容器(如
列表
StringBuilder
),请改用为突变设计的
collect()

如果我更改累加器,使其创建一个
新的Foo
来保存
f.leng+s.length
,而不是将
s.length
添加到现有的
f
,那么它也可以工作。问题不是它“使用得太多”,而是它不是一个身份!
int totalLen = 
    Stream.of(... stuff ...)
          .parallel()
          .mapToInt(String::length)
          .sum();
int totalLen = 
    Stream.of(... stuff ...)
          .parallel()
          .mapToInt(String::length)
          .reduce(0, Integer::sum);