Java8流中的一个简单的list.parallelStream()似乎不起作用?

Java8流中的一个简单的list.parallelStream()似乎不起作用?,java,java-stream,fork-join,Java,Java Stream,Fork Join,从这个问题开始” “,我知道流执行工作窃取。然而,我注意到这种情况似乎并不经常发生。例如,如果我有一个包含100000个元素的列表,并且我试图以parallelStream()的方式处理它,我经常会在最后注意到,我的大多数CPU内核都处于空闲状态,处于“等待”状态。(注意:在列表中的100000个元素中,有些元素需要很长的时间来处理,而另一些则很快;而且,列表是不平衡的,这就是为什么一些线程可能会“不走运”并且有很多事情要做,而另一些线程会走运并且没有什么事情要做) 因此,我的理论是JIT编译器

从这个问题开始” “,我知道流执行工作窃取。然而,我注意到这种情况似乎并不经常发生。例如,如果我有一个包含100000个元素的列表,并且我试图以parallelStream()的方式处理它,我经常会在最后注意到,我的大多数CPU内核都处于空闲状态,处于“等待”状态。(注意:在列表中的100000个元素中,有些元素需要很长的时间来处理,而另一些则很快;而且,列表是不平衡的,这就是为什么一些线程可能会“不走运”并且有很多事情要做,而另一些线程会走运并且没有什么事情要做)

因此,我的理论是JIT编译器将100000个元素初始划分为16个线程(因为我有16个内核),但在每个线程中,它只执行一个简单的(顺序的)for循环(因为这将是最有效的),因此不会发生偷功(这就是我所看到的)


我认为显示工作窃取的原因是有一个外循环在流,一个内循环在流,因此在这种情况下,每个内循环在运行时都会得到评估,并会创建新任务,在运行时可以分配给“空闲”线程。思想?是否有我做错的事情会“强迫”一个简单的list.parallelStream()使用工作窃取?(我当前的解决方法是尝试基于各种heurestics平衡列表,以便每个线程通常看到相同的工作量;但是,很难预测……)

这与JIT编译器无关,而是与流API的实现有关。它将工作负载划分为块,这些块由工作线程按顺序处理。一般的策略是拥有比工作线程更多的作业来实现工作窃取,例如,请参见,可以使用它来实现这种自适应策略

当源是
ArrayList
时,以下代码可用于检测按顺序处理了多少个元素:

List<Object> list = new ArrayList<>(Collections.nCopies(10_000, ""));
System.out.println(System.getProperty("java.version"));
System.out.println(Runtime.getRuntime().availableProcessors());
System.out.println( list.parallelStream()
    .collect(
        () -> new ArrayList<>(Collections.singleton(0)),
        (l,x) -> l.replaceAll(i -> i + 1),
        List::addAll) );

因此,有更多的块而不是核心,以允许工作窃取。然而,一旦一个块的顺序处理开始,它就不能被进一步分割,因此当每个元素的执行时间显著不同时,这种实现就有局限性。这始终是一种权衡。

如果您可以更改默认策略,我会感到惊讶,因为默认策略与您描述的一样简单。我认为这与JIT编译器无关,因为它不应该改变应用程序的行为。这可能与fork-join池的行为更相关,如果
Splitterator
实际上允许拆分。您是否有一个代码示例来演示此行为?我已经做了一些研究,但我对如何按照您的建议“拥有比工作线程更多的作业”感到困惑。是否有一个参数(在流式API中)控制将列表拆分为多少个“作业/任务”?因此,如果我有一个1000000个项目和10个线程的列表,我可以控制是否有每个100000个项目的作业,或者最好是每个500个项目的作业(这样作业可以在当时空闲的线程上运行)?或者,我必须使用ForkJoinTask/Futures来编码这一级别的控制/负载平衡吗?有几个因素决定了实际的策略、实现细节、流源特性、实际的终端操作等。我已经看到,一个策略是只使用预期并行数的四倍。另一种策略是基于我在回答中链接的
get盈余队列taskcount()
方法。它的文档已经说明了这个用例。每个worker分叉,直到此方法报告的值超过某个阈值(应该是一个小数字)。在平衡执行中,这会创建threshold×workers作业,但如果一些作业已经被盗,则会创建更多的作业。@Holger小问题,这是否意味着如果我的
trySplit
实现非常糟糕(或者源代码不是真正可拆分的),我可能根本不会从偷工中获益?另外,我也不太明白这个
625
应该证明什么-我的理解是,这意味着这个10000元素列表将被拆分成
16个拆分器,Eugene打印的结果表明,该测试在一台四芯机器上进行,结果为16块(是
Spliterator
s),因此拆分并不是在每个芯有一个拆分器时停止,而是产生了更多的拆分器。这是至关重要的,因为一旦顺序处理以
forEachRemaining
的方式开始,就不能停止再次拆分工作。但是,当四个内核中的每一个都有四个拆分器要处理时,当内核还没有达到第四个时,就有可能窃取它们。甚至可以想象,一旦发生这种情况,还会进行进一步的分裂。@Holger好极了!我真的很痛苦,在工作中证明了这一点,你的例子使它变得非常简单,不幸的是,我不能投两次赞成票