Java8,在流中使用.parallel会导致OOM错误

Java8,在流中使用.parallel会导致OOM错误,java,parallel-processing,java-8,java-stream,Java,Parallel Processing,Java 8,Java Stream,在《Java8inAction》一书的第7.1.1节中,作者指出流可以通过添加函数.parallel()从并行处理中获益。他们提供了一个名为parallelSum(int)的简单方法来说明这一点。我很想看看它工作得有多好,所以我执行了以下代码: package lambdasinaction.chap7; import java.util.stream.Stream; public class ParallelPlay { public static void main(Strin

在《Java8inAction》一书的第7.1.1节中,作者指出流可以通过添加函数
.parallel()
从并行处理中获益。他们提供了一个名为
parallelSum(int)
的简单方法来说明这一点。我很想看看它工作得有多好,所以我执行了以下代码:

package lambdasinaction.chap7;

import java.util.stream.Stream;

public class ParallelPlay {

    public static void main(String[] args) {
        System.out.println(parallelSum(100_000_000));
    }

    public static long parallelSum(long n) {
        return Stream.iterate(1L, i -> i + 1)
                .limit(n)
                .parallel()
                .reduce(0L, Long::sum);
    }
}
令我惊讶的是,我收到了以下错误:

Exception in thread "main" java.lang.OutOfMemoryError
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
    at java.lang.reflect.Constructor.newInstance(Unknown Source)
    at java.util.concurrent.ForkJoinTask.getThrowableException(Unknown Source)
    at java.util.concurrent.ForkJoinTask.reportException(Unknown Source)
    at java.util.concurrent.ForkJoinTask.invoke(Unknown Source)
    at java.util.stream.SliceOps$1.opEvaluateParallelLazy(Unknown Source)
    at java.util.stream.AbstractPipeline.sourceSpliterator(Unknown Source)
    at java.util.stream.AbstractPipeline.evaluate(Unknown Source)
    at java.util.stream.ReferencePipeline.reduce(Unknown Source)
    at lambdasinaction.chap7.ParallelPlay.parallelSum(ParallelPlay.java:15)
    at lambdasinaction.chap7.ParallelPlay.main(ParallelPlay.java:8)
Caused by: java.lang.OutOfMemoryError: Java heap space
    at java.util.stream.SpinedBuffer.ensureCapacity(Unknown Source)
    at java.util.stream.Nodes$SpinedNodeBuilder.begin(Unknown Source)
    at java.util.stream.AbstractPipeline.copyInto(Unknown Source)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
    at java.util.stream.SliceOps$SliceTask.doLeaf(Unknown Source)
    at java.util.stream.SliceOps$SliceTask.doLeaf(Unknown Source)
    at java.util.stream.AbstractShortCircuitTask.compute(Unknown Source)
    at java.util.concurrent.CountedCompleter.exec(Unknown Source)
    at java.util.concurrent.ForkJoinTask.doExec(Unknown Source)
    at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(Unknown Source)
    at java.util.concurrent.ForkJoinPool.runWorker(Unknown Source)
    at java.util.concurrent.ForkJoinWorkerThread.run(Unknown Source)

我在Windows7和SP1上运行Java1.8.045,使用四核处理器。发生了什么事?

在这里,您创建了一个无限流,然后对其进行限制。关于并行处理无限流,存在一些已知的问题。特别是,无法有效地将任务分成相等的部分。在内部使用了一些启发式方法,这些方法并不适合于每项任务。在您的情况下,最好使用
LongStream.range
创建有限流:

import java.util.stream.LongStream;

public class ParallelPlay {

    public static void main(String[] args) {
        System.out.println(parallelSum(100_000_000));
    }

    public static long parallelSum(long n) {
        return LongStream.rangeClosed(1, n).parallel().sum();
    }
}
在这种情况下,流引擎从一开始就知道您有多少元素,因此它可以有效地分割任务。还要注意的是,使用
LongStream
更有效,因为您将没有不必要的装箱


一般来说,如果你能用有限的数据流来解决你的任务,就要避免无限的数据流。

在macbook pro(2.2 GHz Intel Core i7,16GB ram)上,它花了26秒时间返回:50000005000000看起来你的堆大小太小了,运行:
java-XX:+PrintFlagsFinal-version | findstr/i“HeapSize PermSize ThreadStackSize”
检查它,并考虑增加它(通过改变<代码> -xMS和<代码> - Xmx < /代码>),并再次尝试运行。此外,使用<代码>迭代()/代码>作为流源,基本上保证不会获得任何并行化,因为这是从根本上连续生成的。(在生成元素n之前无法生成元素n+1。)请改用
IntStream.range()
。尽管如此,无论有多少线程可以并行运行,它们都无法打败简单的单线程
(n+1)*n/2
。不幸的是,标准实现不够聪明,无法理解这一点。@Holger:这是一个有趣的问题,为什么没有专门的流(如
EmptyStream
SingletonStream
RangeStream
等)可以优化某些操作。这可能会产生太多的代码(考虑到您也应该支持原语的事实)。另一个可能的原因是,在正常情况下,流调用将变得多态,这可能会影响性能,因此JIT将更难对其进行设备化(目前JDK只有一个
实现)。我不认为多态流实现比多态
拆分器
接口对性能的影响更大。毕竟,Hotspot在处理这些接口方面做得很好,终端操作只包含一个方法调用。但你仍然不需要它们,你唯一需要的就是拥有它们(或某些)在内部管道阶段上定义的高级操作。通常情况下,要么没有人考虑它,要么优化被推迟。请参阅Java 9中的捷径
Stream.count()
,或者还不懒惰的flatmap流。@Holger,是的,我已经尝试过优化JDK9
Stream.count()
。当使用非平凡的
flatMap
进行大收集时,优化可能非常重要:在这种情况下,流操作会执行多次。但我同意:我的StreamEx库添加了更多的流实现,基准很少显示可见的性能开销。可能没有完成,因为
RangeStream
can只优化很少出现在实际代码中的非常琐碎的情况。例如,很难优化
IntStream.range(0100).map(x->x*2.sum()
。事实上,这不是真的(关于无限流)。您可以用:
LongStream.generate(()->1L).limit(n).parallel().reduce(0L,Long::sum)重写上面的示例
并且它不会因OOM而失败。