在Java 8中,用两个返回Stream<E>的方法扩展了Collection接口:stream()返回一个顺序流,而parallelStream()返回一个可能并行的流。 Stream本身还具有parallel()方法,该方法返回等效的并行流(将当前流更改为并行或创建新流)。

复制有明显的缺点:


令人困惑。给定parallelStream()可能返回顺序流的问题,因此会问whether calling both parallelStream().parallel() is necessary to be sure the stream is parallel。如果不能保证parallelStream()为什么存在,为什么?另一种方法也是令人困惑的-如果parallelStream()返回顺序流,则可能是有原因的(例如,并行流本身就是性能陷阱的固有顺序数据结构); Stream.parallel()对这样的流应该做什么? (parallel()的规范不允许UnsupportedOperationException。)
如果现有实现具有名称相似且返回类型不兼容的方法,则将方法添加到接口可能会发生冲突。除了stream()之外,添加parallelStream()会使获得很少收益的风险加倍。 (请注意,parallelStream()在某一时刻仅被命名为parallel(),尽管我不知道是否对其进行了重命名以避免名称冲突或其他原因。)


为什么在调用Collection.stream()。parallel()时存在Collection.parallelStream()同样的事情?

最佳答案

Collection.(parallelS|s)tream()Stream的Javadocs本身无法回答问题,因此有关原理的信息已转至邮件列表。我浏览了lambda-libs-spec-observers档案,找到了one thread specifically about Collection.parallelStream()和另一个线程,该线程触及java.util.Arrays should provide parallelStream()是否匹配(或者实际上是否应该删除)。没有一劳永逸的结论,因此也许我错过了另一个清单中的某些内容,或者此事已在私人讨论中解决。 (也许Brian Goetz是此讨论的原理之一,可以弥补任何遗漏的内容。)

参与者的观点很好,因此,答案基本上只是相关引语的组织,在[方括号]中有一些澄清,以重要性的顺序表示(据我解释)。

parallelStream()涵盖了一个非常常见的情况

第一个线程中的Brian Goetz,解释了为什么即使删除了其他并行流工厂方法后,Collections.parallelStream()仍然足以保留的价值:


  我们没有每个[流工厂]的显式并行版本。我们做了
  最初,为了减少API表面积,我们在
  从API中删除20多种方法值得进行权衡的理论
  .intRange(...).parallel()的表面皱纹和性能成本。
    但是我们没有使用Collection做出选择。
  
  我们可以删除Collection.parallelStream(),也可以添加
  所有生成器的并行版本,否则我们什么也做不了,
  保持原样。我认为所有这些在API设计方面都是合理的。
  
  尽管有点不一致,但我还是很喜欢现状。代替
  有2N个流构建方法,我们有N + 1个-但是那额外的1个
  涵盖了很多情况,因为它是每个人都继承的
  采集。所以我可以为自己辩解为什么要有这种额外的1种方法
  是值得的,为什么接受不再走一步的矛盾是
  可以接受的。
  
  别人不同意吗? N + 1 [仅适用于Collections.parallelStream()]是这里的实际选择吗?还是应该去
  N的纯度[依靠Stream.parallel()]?还是2N(所有工厂的并行版本)的便利性和一致性?或者是
  还有一些更好的N + 3 [Collections.parallelStream()以及其他特殊情况],对于其他一些特殊情况,我们
  想给予特别支持吗?


Brian Goetz在后面有关Arrays.parallelStream()的讨论中坚持这一立场:


  我还是很喜欢Collection.parallelStream;它有巨大的
  可发现性的优势,并在API上带来可观的回报
  表面积-另一种方法,但可以在许多地方提供价值,
  因为Collection是流源的真正常见情况。


parallelStream()性能更高

Brian Goetz


  直接版本[parallelStream()]的性能更高,因为它需要更少的包装(
  将流变成并行流,您必须先创建
  顺序流,然后将其状态的所有权转移到新
  流。)


为了回应Kevin Bourrillion关于效果是否显着的怀疑,Brian again


  取决于您计数的认真程度。道格计算单个对象
  在进行并行操作的过程中进行创建和虚拟调用,
  因为在开始分叉之前,您处于Amdahl的错误立场
  法律-这就是在分叉之前发生的所有“序列分数”
  任何工作,将您的收支平衡极限进一步提高。所以得到
  快速并行操作的设置路径很有价值。


Doug Lea follows up,但对冲他的位置:


  处理并行库支持的人员需要一些态度
  调整这类事情。在即将成为典型的机器上,
  您浪费的每个周期设置并行度都需要花费64个周期。
  如果需要64,您可能会有不同的反应
  对象创建以开始并行计算。
  
  也就是说,我始终完全支持强制实施者
  为了更好的API而努力工作,只要
  API并不排除有效的实现。所以如果杀死
  parallelStream非常重要,我们将找到一些方法来
  将stream().parallel()变成位翻转。


确实,稍后有关Arrays.parallelStream() takes notice of lower Stream.parallel() cost的讨论。

stream()。parallel()有状态使未来复杂化

在讨论时,可以将流从顺序切换到并行再切换回去可以与其他流操作交错进行。 Brian Goetz, on behalf of Doug Lea,解释了为什么顺序/并行模式切换会使Java平台的未来开发变得复杂:


  我将竭尽全力解释原因:因为它(例如有状态
  您也不喜欢的方法(排序,不同,限制))
  距离能够表达流管道越来越远
  传统数据并行构造的术语,这进一步限制了
  我们将它们直接映射到明天的计算基础上的能力,
  无论是矢量处理器,FPGA,GPU还是我们自己准备的东西。
  
  Filter-map-reduce映射非常干净地用于各种并行计算
  基材过滤器并行映射顺序排序限制并行映射uniq减少
  才不是。
  
  因此,这里的整个API设计在设计之间体现出许多张力
  易于表达用户可能想要表达的事物,并且正在做
  可以使我们以透明的成本快速实现预期目标
  楷模。


此模式切换为removed after further discussion。在该库的当前版本中,流管道是顺序的或并行的。对sequential() / parallel()的最后一次呼叫获胜。除了回避状态问题之外,此更改还提高了使用parallel()设置顺序流工厂的并行管道的性能。

将parallelStream()公开为一等公民可以提高程序员对库的认识,从而使他们编写更好的代码

Brian Goetz again,作为对Tim Peierls's argument的回应,Stream.parallel()允许程序员在并行之前顺序地了解流:


  我对这个顺序的价值略有不同的看法
  直觉-我认为普遍的“顺序期望”是一种
  这整个工作的最大挑战;人们不断
  带来不正确的顺序偏见,这会使他们变得愚蠢
  例如使用单元素数组来“欺骗”“愚蠢”的方法
  编译器让他们捕获可变的本地变量,或者使用lambda作为
  映射将在
  计算(以非线程安全的方式),然后指出
  他们在做什么,耸耸肩说:“是的,但我没有
  并行进行。”
  
  我们进行了很多设计折衷,以合并顺序和并行
  流。我认为,结果是干净的,并将增加
  图书馆在十年以上仍然有用的机会,但我没有
  特别像鼓励人们认为这是一个
  顺序库,侧面装有一些平行袋。

10-05 17:59