Java并行流的性能影响

Java并行流的性能影响,java,concurrency,parallel-processing,java-stream,forkjoinpool,Java,Concurrency,Parallel Processing,Java Stream,Forkjoinpool,使用.stream().parallel()的最佳实践是什么 例如,如果您有一组阻塞I/O调用,并且希望检查.anyMatch(…),那么并行执行这项操作似乎是明智的 示例代码: public boolean hasAnyRecentReference(JobId jobid) { <...> return pendingJobReferences.stream() .parallel() .anyMatch(pendingRef -> {

使用
.stream().parallel()
的最佳实践是什么

例如,如果您有一组阻塞I/O调用,并且希望检查
.anyMatch(…)
,那么并行执行这项操作似乎是明智的

示例代码:

public boolean hasAnyRecentReference(JobId jobid) {
  <...>
  return pendingJobReferences.stream()
     .parallel()
     .anyMatch(pendingRef -> { 
       JobReference readReference = pendingRef.sync();
       Duration referenceAge = timeService.timeSince(readReference.creationTime());
       return referenceAge.lessThan(maxReferenceAge)
     });
}
public boolean hasAnyRecentReference(JobId JobId){
返回pendingJobReferences.stream()
.parallel()
.anyMatch(pendingRef->{
JobReference readReference=pendingRef.sync();
Duration referenceAge=timeService.timeSince(readReference.creationTime());
返回referenceAge.lessThan(maxReferenceAge)
});
}
乍一看,这看起来很合理,因为我们可以同时执行多个阻塞读取,因为我们只关心任何匹配项,而不是逐个检查(因此,如果每次读取都需要50毫秒,我们只需等待(50毫秒*expectedNumberOfNonRecentRefs)/numThreads)


在生产环境中引入此代码会对代码库的其他部分产生任何不可预见的性能影响吗?

编辑:正如@edharned指出的那样
。parallel()
现在使用
CountedCompleter
而不是调用
。join()
,Ed在
当前正在做什么?
一节中对其自身存在的问题进行了解释

我相信下面的信息对于理解为什么fork-join框架很棘手仍然很有用,并且在结论中提出的
.parallel()
替代方案仍然是相关的


虽然代码的精神是正确的,但实际的代码可能会对所有使用
.parallel()
的代码产生系统范围的影响,尽管这一点并不明显

不久前,我发现一篇文章建议不要这样做,但直到最近我才深入研究

以下是我读了一大堆书后的想法:

  • Java中的
    .parallel()
    使用
    ForkJoinPool.commonPool()
    这是一个由所有流共享的单例
    ForkJoinPool
    ForkJoinPool.commonPool()
    是一个公共静态方法,因此理论上其他库/代码部分可以使用它)
  • ForkJoinPool
    实现工作窃取,除了共享队列外,还具有每线程队列

  • 工作窃取意味着当线程空闲时,它会寻找更多的工作来做
  • 起初我想:根据这个定义,
    缓存的
    线程池不也会进行工作窃取吗(即使一些引用称之为缓存线程池的工作共享)
  • 事实证明,在使用idle这个词时,似乎存在一些术语模糊性:

  • 在缓存的线程池中,线程只有在完成任务后才处于空闲状态。如果它在等待阻塞呼叫时被阻塞,它不会变为空闲
  • forkjoin
    threadpool中,线程在完成任务或调用子任务上的
    .join()
    方法(这是一个特殊的阻塞调用)时处于空闲状态

    当对子任务调用
    .join()
    时,线程在等待该子任务完成时变为空闲。空闲时,它将尝试执行任何其他可用任务,即使它在另一个线程的队列中(它窃取工作)

    [这是重要的一点]一旦找到另一个要执行的任务,它必须在恢复其原始执行之前完成它,即使它等待的子任务在线程仍在执行偷来的任务时完成

    [这也很重要]此工作窃取行为仅适用于调用
    .join()
    的线程。如果一个线程被其他东西阻塞,比如I/O,它就会变为空闲(即它不会窃取工作)

  • Java streams不允许您提供自定义的ForkJoinPool,但允许您提供自定义的ForkJoinPool

  • 我花了一段时间才理解
    2.3.2
    的含义,因此我将举一个简单的例子来帮助说明这个问题:

    注意:这些都是虚构的例子,但是您可以通过使用streams(在内部执行fork-join操作)进入相同的情况,而不会意识到这一点

    此外,我将使用极其简化的伪代码,它只用于说明.parallel()问题,但在其他方面不一定有意义

    假设我们正在实现合并排序

    merge_sort(list):
        left, right = split(list)
    
        leftTask = mergeSortTask(left).fork()
        rightTask = mergeSortTaks(right).fork()
    
        return merge(leftTask.join(), rightTask.join())
    
    现在让我们假设我们有另一段代码,它执行以下操作:

    dummy_collect_results(queriesIds):
       pending_results = []
    
       for id in queriesIds: 
         pending_results += longBlockingIOTask(id).fork()
    
      // do more stuff
    
    这里发生了什么?

    在编写合并排序代码时,您认为排序调用不会执行任何I/O操作,因此它们的性能应该是非常确定的,对吗

    对。您可能没有料到的是,由于
    dummy\u collect\u results
    方法创建了一组长时间运行和阻塞的子任务,当执行mergesort任务的线程阻塞
    .join()
    ,等待子任务完成时,它们可能会开始执行其中一个长时间阻塞的子任务

    这是不好的,因为如上所述,一旦长阻塞(在I/O上,不是一个
    .join()
    调用,这样线程就不会再次变为空闲)被窃取,它就必须完成,而不管线程在阻塞I/O时是否通过
    .join()
    完成了子任务

    这使得mergesort任务的执行不再具有确定性,因为执行这些任务的线程可能最终窃取完全位于其他地方的代码在中生成的I/O密集型任务

    这也是非常可怕和难以理解的,因为您可以在整个代码库中使用
    .parallel()
    ,而不会出现任何问题,并且只需要一个类在使用
    .parallel()
    时引入长时间运行的任务,突然之间,代码库的所有其他部分可能会获得不一致的性能

    所以我的结论是: