并行无限Java流内存不足


16

我试图理解为什么以下Java程序给出OutOfMemoryError,而没有的相应程序没有给出.parallel()

System.out.println(Stream
    .iterate(1, i -> i+1)
    .parallel()
    .flatMap(n -> Stream.iterate(n, i -> i+n))
    .mapToInt(Integer::intValue)
    .limit(100_000_000)
    .sum()
);

我有两个问题:

  1. 该程序的预期输出是什么?

    如果没有,.parallel()这似乎只是输出sum(1+2+3+...),这意味着它只是在flatMap的第一个流“卡住”了,这是有道理的。

    对于并行,我不知道是否有预期的行为,但是我的猜测是它以某种方式交错了第一个n左右的流,n并行工作者的数量在哪里。根据组块/缓冲行为,它也可能略有不同。

  2. 是什么导致它的内存不足?我正在专门尝试了解这些流是如何在后台实现的。

    我猜有些东西阻塞了流,因此它永远不会完成,并且能够摆脱生成的值,但是我不太清楚事物的评估顺序和缓冲发生的位置。

编辑:如果相关,我正在使用Java 11。

Editt 2:即使对于简单的程序IntStream.iterate(1,i->i+1).limit(1000_000_000).parallel().sum(),显然也会发生同样的事情,因此它可能与limit而不是的懒惰有关flatMap


parallel()内部使用ForkJoinPool。我想ForkJoin框架是Java从Java 7
亚拉文

Answers:


9

您说“ 但我不太清楚事物的评估顺序和发生缓冲的位置 ”,这正是并行流的含义。评估顺序不确定。

您的示例的一个关键方面是.limit(100_000_000)。这意味着该实现不仅可以求和任意值,还必须求和前100,000,000个数字。请注意,在参考实现中,.unordered().limit(100_000_000)不会更改结果,这表明无序情况没有特殊的实现,但这只是实现细节。

现在,当工作线程处理这些元素时,它们不能仅仅对其进行汇总,因为它们必须知道允许使用哪些元素,这取决于在其特定工作负载之前有多少个元素。由于此流不知道大小,因此只有在处理了前缀元素后才能知道,而无限流则永远不会发生。因此,工作线程暂时保持缓冲状态,此信息变为可用。

原则上,当工作线程知道它正在处理最左边的工作块时,它可以立即求和,计数并在达到极限时发出结束信号。因此,Stream可能会终止,但这取决于很多因素。

在您的情况下,一个合理的情况是其他工作线程在分配缓冲区时比最左边的作业要快。在这种情况下,时间上的细微变化可能会使流偶尔返回一个值。

当我们减慢所有工作线程(除了处理最左边的块的那个线程之外)时,我们可以使流终止(至少在大多数运行中):

System.out.println(IntStream
    .iterate(1, i -> i+1)
    .parallel()
    .peek(i -> { if(i != 1) LockSupport.parkNanos(1_000_000_000); })
    .flatMap(n -> IntStream.iterate(n, i -> i+n))
    .limit(100_000_000)
    .sum()
);

¹我遵循Stuart Marks的建议,在谈论相遇顺序而不是处理顺序时使用从左到右的顺序。


很好的答案!我想知道是否所有线程都开始运行flatMap操作,而实际上没有分配任何缓冲区来清空缓冲区(求和)的风险?在我的实际用例中,无限流是文件太大而无法保存在内存中。我想知道如何重写流以降低内存使用量吗?
Thomas Ahle

1
您正在使用Files.lines(…)吗?已显著在Java中9.改进
霍尔格

1
这就是在Java 8中所做的事情。在较新的JRE中,BufferedReader.lines()在某些情况下(它不是默认的文件系统,特殊的字符集或大于的大小Integer.MAX_FILES),它仍然会使用。如果其中一种适用,那么定制解决方案可能会有所帮助。这将值得进行新的问答…
Holger

1
Integer.MAX_VALUE,当然…
Holger

1
什么是外部流,文件流?它有可预测的大小吗?
Holger

5

我最好的猜测是,将parallel()改变的内部行为flatMap(),其懒惰之前被评价已经出现了问题

OutOfMemoryError[JDK-8202307]中报告了您遇到的错误,该错误是在使用flatMap中无限大的Stream的流上调用Stream.iterator()。next()时获取java.lang.OutOfMemoryError:Java堆空间。如果您查看票证,它或多或少与您获得的堆栈跟踪相同。由于无法修复,该票证已关闭,原因如下:

iterator()spliterator()方法“逃生舱”被使用时,它不是可以使用其它操作。它们有一些局限性,因为它们将流实现的推送模型转换为拉模型。这种过渡在某些情况下需要缓冲,例如当一个元素(平面)映射到两个或多个元素时。这可能会大大增加流实现的复杂性,可能会以牺牲普通情况为代价来支持背压的概念,以传达多少元素要拉过元素生产的嵌套层。


这很有趣!推/拉转换需要缓冲可能会占用内存,这是有道理的。但是在我的情况下,似乎仅使用推应该可以正常工作,并且仅丢弃出现的其余元素?或者,也许您是说flapmap导致创建迭代器?
Thomas Ahle

3

OOME的原因不是流无限,而是由于事实并非如此

即,如果您注释掉.limit(...),它将永远不会耗尽内存-但是,当然,它也永远不会结束。

拆分后,如果元素在每个线程中累积,则流只能跟踪它们的数量(看起来像实际的累加器是Spliterators$ArraySpliterator#array)。

看起来您可以不使用它而重现它flatMap,只需使用以下命令运行以下命令-Xmx128m

    System.out.println(Stream
            .iterate(1, i -> i + 1)
            .parallel()
      //    .flatMap(n -> Stream.iterate(n, i -> i+n))
            .mapToInt(Integer::intValue)
            .limit(100_000_000)
            .sum()
    );

但是,在注释掉后limit(),它应该可以正常运行,直到您决定腾出您的笔记本电脑为止。

除了实际的实现细节之外,我还认为这是正在发生的事情:

使用limitsumreducer希望对前X个元素求和,因此没有线程可以发出部分和。每个“切片”(线程)将需要累积元素并传递它们。没有限制,没有这样的约束,因此每个“切片”将只是(最终)从它获取的元素(永远)中计算出部分和,假设它最终会发出结果。


你是什​​么意思“一旦分裂”?极限会以某种方式拆分吗?
Thomas Ahle

@ThomasAhle parallel()将在ForkJoinPool内部使用以实现并行性。在Spliterator将被用于工作分派给每个ForkJoin任务,我想我们可以在这里称之为工作单位为“分”。
Karol Dowbecki

但是,为什么只发生极限?
Thomas Ahle

@ThomasAhle我用两分钱编辑了答案。
Costi Ciudatu

1
@ThomasAhle Integer.sum()IntStream.sum减速器中使用设置一个断点。您会看到无限制版本始终会调用该函数,而受限版本永远不会在OOM之前调用它。
Costi Ciudatu
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.