为什么具有short-curcuit操作的并行Java Stream会评估Stream的所有元素,而顺序Stream则不会?

时间:2017-10-10 18:50:15

标签: java parallel-processing java-8 java-stream short-circuiting

考虑两个测试方法parallel()和sequential():

  @Test
  public void parallel() throws Exception
  {
    System.out.println( "parallel start." );
    IntStream.of( 0, 1 ).parallel().map( this::work ).findAny();
    System.out.println( "parallel done." );
  }

  @Test
  public void sequential() throws Exception
  {
    System.out.println( "sequential start." );
    IntStream.of( 0, 1 ).map( this::work ).findAny();
    System.out.println( "sequential done." );
  }

  private int work(int i)
  {
    System.out.println( "working... " + i );
    Threads.sleepSafe( i * 1000 );
    System.out.println( "worked. " + i );
    return i;
  }

Threads.sleepSafe()是一个围绕Thread.sleep()的简单包装器,它吞下异常,如果传递0则不执行任何操作。

运行测试方法时,结果如下:

sequential start.
working... 0
worked. 0
sequential done.

parallel start.
working... 1
working... 0
worked. 0
sleeping for 1000 ms ...
slept for 1000 ms.
worked. 1
parallel done.

sequential()按照我的预期运作,但parallel()没有: 我希望findAny()中的parallel()work()第一次返回时返回(即对于值0,因为它不会休眠),而是仅在{{1}之后返回1}}也完成了值1。

为什么?

首次work()返回时,有没有办法让findAny()返回?

3 个答案:

答案 0 :(得分:3)

并行模式下的流API基于ForkJoinPool范例,默认情况下使用max X 线程(其中 X 等于可用数量)处理器)。如果您将增加迭代次数,则可以检查此规则。

通常,并行流的默认线程池计数可以通过两种方式进行自定义:

  • 将并行流执行提交给您自己的ForkJoinPool:yourFJP.submit(() -> stream.parallel().forEach(soSomething));
  • 使用系统属性更改公共池的大小:System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "20"),以获得20个线程的目标并行度。
  

有没有办法让work()在第一次返回时返回findAny()?

根据ForkJoin算法的想法,答案基本上是。它"等待"而所有的线程都将完成他们的工作。但正如之前所述,您可以将工人数量限制为单个工人。显然它不会产生任何场景,因为这种方法与顺序执行类似,并且冗余操作会带来额外的开销。

答案 1 :(得分:2)

并行流仍然支持短路,但是如果所有线程都推迟了他们的工作,直到处理前一个元素的线程确认操作尚未结束,那么使用并行流没有任何优势。

因此,只要最终结果正确组合,即丢弃多余的元素,并行流就会处理超过必要数量的未指定数量的元素。

这只是你的例子,只包含两个元素,只是处理一个超过必要的元素,可以解释为“所有元素都被处理”。

当元素数量很少和/或实际操作是找到可预测地属于流的第一个元素之一时,并行处理通常没什么好处。如果您执行类似

的操作,事情会变得更有趣
IntStream.range(0, 2000).parallel()
    .map(i -> { LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(50)); return i;})
    .filter(i->i%397==396)
    .findAny();

请注意,终端操作将在返回最终结果之前等待所有工作线程的完成,因此当在找到结果时已经开始评估元素时,将完成该元素的处理。这是设计的。它确保在流操作后应用程序代码继续执行时,不会对lambda表达式访问的源集合或其他数据进行并发访问。

the package documentation比较:

  

在几乎所有情况下,终端操作都是 eager ,在返回之前完成对数据源的遍历和管道的处理。只有终端操作iterator()spliterator()不是; ...

因此,短路并行流不会处理所有元素,但是当其他工作线程仍在处理过时元素时,可能仍需要更长的时间来返回已经评估的结果。

如果您希望提前返回,接受可能仍在运行的后台线程,则Stream API不适合您。考虑

private int work(int i) throws InterruptedException {
    System.out.println( "working... " + i );
    Thread.sleep(i * 1000);
    System.out.println( "worked. " + i );
    return i;
}
public void parallel() throws Exception {
    System.out.println( "parallel start." );
    List<Callable<Integer>> jobs = IntStream.range(0, 100)
      .collect(ArrayList::new, (l,i) -> l.add(() -> work(i)), List::addAll);
    ExecutorService pool = Executors.newFixedThreadPool(10);
    Integer result = pool.invokeAny(jobs);
    pool.shutdown();
    System.out.println( "parallel done, result="+result );
}

请注意,这不仅会在第一个作业完成后立即返回,还支持通过中断取消所有已在运行的作业。

答案 2 :(得分:1)

如果你想要一个并行流,那么是的,它会同时多次调用work方法。

请注意,如果并行流包含1,000个元素并使用5个线程,则并行流最多会调用work 5次,而不是1,000次。

如果您只想拨打work一次,请使用顺序信息流。