环境:Ubuntu x86_64(14.10),Oracle JDK 1.8u25
我尝试使用Files.lines()
的并行流,但我想.skip()
第一行(它是带有标题的CSV文件)。所以我试着这样做:
try (
final Stream<String> stream = Files.lines(thePath, StandardCharsets.UTF_8)
.skip(1L).parallel();
) {
// etc
}
但是后来一列未能解析为int ...
所以我尝试了一些简单的代码。文件问题很简单:
$ cat info.csv
startDate;treeDepth;nrMatchers;nrLines;nrChars;nrCodePoints;nrNodes
1422758875023;34;54;151;4375;4375;27486
$
代码同样简单:
public static void main(final String... args)
{
final Path path = Paths.get("/home/fge/tmp/dd/info.csv");
Files.lines(path, StandardCharsets.UTF_8).skip(1L).parallel()
.forEach(System.out::println);
}
我系统地获得以下结果(好的,我只运行了大约20次):
startDate;treeDepth;nrMatchers;nrLines;nrChars;nrCodePoints;nrNodes
我在这里缺少什么?
编辑似乎问题或误解比这更加根深蒂固(下面的两个例子是由FreeNode的## java编写的):
public static void main(final String... args)
{
new BufferedReader(new StringReader("Hello\nWorld")).lines()
.skip(1L).parallel()
.forEach(System.out::println);
final Iterator<String> iter
= Arrays.asList("Hello", "World").iterator();
final Spliterator<String> spliterator
= Spliterators.spliteratorUnknownSize(iter, Spliterator.ORDERED);
final Stream<String> s
= StreamSupport.stream(spliterator, true);
s.skip(1L).forEach(System.out::println);
}
打印:
Hello
Hello
呃。
@Holger建议对于使用此另一个示例的ORDERED
而非SIZED
的任何流都会发生这种情况:
Stream.of("Hello", "World")
.filter(x -> true)
.parallel()
.skip(1L)
.forEach(System.out::println);
此外,它源于已经发生的所有讨论,问题(如果是一个?)与.forEach()
(作为@SotiriosDelimanolis first pointed out)。
答案 0 :(得分:18)
这个答案已经过时 - 请阅读THIS ONE INSTEAD!
快速回答问题:观察到的行为是有意的!根据文档,没有任何错误,所有情况都在发生。但是,可以说,这种行为应该被记录下来并更好地传达。 forEach
忽略排序应该更加明显。
我首先介绍允许观察到的行为的概念。这为解析问题中给出的示例之一提供了背景。我将在高级别上执行此操作,然后再在非常低级别上执行此操作。
[TL; DR:自行阅读,高级别解释将给出一个粗略的答案。]
让我们谈谈流操作和流管道,而不是谈论Stream
,这是由流相关方法操作或返回的类型。方法调用lines
,skip
和parallel
是流操作,它构建流管道[1],并且 - 正如其他人所指出的那样 - 管道在终端操作时作为一个整体进行处理{ {1}}被称为[1]。
管道可以被认为是一系列操作,它们一个接一个地在整个流上执行(例如,过滤所有元素,将剩余元素映射到数字,对所有数字求和)。 但这是误导性的!更好的比喻是终端操作通过每个操作拉出单个元素[3](例如,获取下一个未过滤的元素,映射它,将其添加到sum,请求下一个元素)。某些中间操作可能需要遍历多个(例如forEach
)或甚至所有(例如skip
)元素,然后才能返回所请求的下一个元素,这是操作中状态的来源之一。
每个操作都用这些StreamOpFlag
s表示其特征:
sort
DISTINCT
SORTED
ORDERED
SIZED
它们在流源,中间操作和终端操作中组合在一起,构成管道的特征(作为一个整体),然后用于优化[4]。类似地,管道是否并行执行是整个管道的属性[5]。
因此,无论何时对这些特征做出假设,您都必须仔细查看构建管道的所有操作,无论它们的应用顺序如何,以及它们的保证。这样做时请记住终端操作如何通过管道拉动每个单独的元素。
让我们来看看这个特例:
SHORT_CIRCUIT
无论您的流源是否已订购(通过调用BufferedReader fooBarReader = new BufferedReader(new StringReader("Foo\nBar"));
fooBarReader.lines()
.skip(1L)
.parallel()
.forEach(System.out::println);
(而不是forEach
),您都声明订单对您不重要< / strong> [6],有效地减少了forEachOrdered
来自&#34;跳过第一个 n 元素&#34; to&#34;跳过任何 n 元素&#34; [7](因为没有顺序,前者变得毫无意义)。
所以如果承诺加速,你就给管道权利忽略秩序。对于并行执行,它显然是这么认为的,这就是为什么你得到观察到的输出。因此你观察到的是预期的行为而且没有错误。
请注意,此与skip
有状态不会发生冲突!如上所述,有状态并不意味着它以某种方式缓存整个流(减去跳过的元素),并且随后的所有内容都在这些元素上执行。它只是意味着操作有一些状态 - 即跳过的元素的数量(好吧,它实际上不是that easy,但是由于我对正在发生的事情的理解有限,我不知道说这是一个公平的简化。)
让我们更详细地看一下:
skip
创建BufferedReader.lines
,我们称之为Stream
:
Spliterator
_lines
,这会创建ReferencePipeline.Head
并将spliterator标志转换为流op标志StreamSupport.stream
创建一个新的.skip
,让我们称之为Stream
:
_skip
SliceOps.makeRef
ReferencePipeline.skip
的匿名实例,该实例引用ReferencePipeline.StatefulOp
作为其来源_lines
设置整个管道的并行标志,如上所述.parallel
实际上开始执行让我们看看管道是如何执行的:
_skip.forEach
会创建ForEachOp
(让我们称之为.forEach
)并将其交给_skip.evaluate
,这会做两件事:
sourceSpliterator
围绕此管道阶段的源创建一个分裂器:
opEvaluateParallelLazy
(事实证明)UnorderedSliceSpliterator
(让我们称之为_forEach
)_sliceSpliterator
且没有限制。_forEach.evaluateParallel
创建ForEachTask
(因为它是无序的;让我们称之为skip = 1
)并调用它_forEachTask.compute
任务中拆分前1024行,为它创建一个新任务(让我们称之为_forEachTask
),意识到没有任何行留下并完成。_forEachTask2.compute
被调用,通过调用finally starts copying its elements into the sink徒劳地尝试再次拆分_skip.copyInto
(_forEachTask2
周围的流感知包装器)。 System.out.println
!因此_sliceSpliterator.forEachRemaining
负责将未跳过的元素处理到println-sink:
acquirePermits
所以UnorderedSliceSpliterator.OfRef.forEachRemaining
是订单最终真正被忽略的地方。我没有将它与有序变体进行比较,但这是我为什么这样做的假设:
有任何问题吗? ;)抱歉这么久了。也许我应该省略细节并撰写博客文章......
[1] java.util.stream
- Stream operations and pipelines:
流操作分为中间和终端操作,并组合成流管道。
[2] java.util.stream
- Stream operations and pipelines:
在执行管道的终端操作之前,不会开始遍历管道源。
[3]这个比喻代表了我对溪流的理解。除了代码之外,主要来源是java.util.stream
- Stream operations and pipelines(突出我的):
懒洋洋地处理流可以显着提高效率;在诸如上面的filter-map-sum示例的流水线中,过滤,映射和求和可以融合到数据的单个传递中,具有最小的中间状态。懒惰还允许在不必要时避免检查所有数据;对于诸如&#34;之类的操作,找到超过1000个字符的第一个字符串&#34;,只需要检查足够的字符串以找到具有所需特征的字符串,而不检查源中可用的所有字符串。
[4] java.util.stream.StreamOpFlag
:
在管道的每个阶段,可以计算组合的流和操作标志[... jadda,jadda,jadda,关于如何在源,中间和终端操作中组合标志 ... ]生成管道输出的标志。然后可以使用这些标志来应用优化。
在代码中,您可以在AbstractPipeline.combinedFlags
中看到这一点,它是在构造期间(以及在其他一些事件中)通过组合前一个操作和新操作的标志来设置的。
[5] java.util.stream
- Parallelism(我无法直接链接 - 向下滚动一下):
启动终端操作时,流管道按顺序或并行执行,具体取决于调用它的流的方向。
在代码中,您可以看到它位于AbstractPipeline.sequential
, parallel
和isParallel
,它在流源上设置/检查布尔标志,使得在构造流时调用setter时无关紧要。 / p>
[6] java.util.stream.Stream.forEach:
对此流的每个元素执行操作。 [...]此操作的行为明确是不确定的。
将此与java.util.stream.Stream.forEachOrdered对比:
如果流具有已定义的遭遇顺序,则按流的遭遇顺序对此流的每个元素执行操作。
[7]这也没有明确记载,但我对Stream.skip
的评论的解释(由我严重缩短):
[...] skip()[...]在有序并行流水线上可能非常昂贵[...]因为skip(n)被约束为不仅跳过任何n个元素,而是跳过前n个元素遇到订单。 [...] [R]提供排序约束[...]可能会导致并行管道中skip()的显着加速
答案 1 :(得分:17)
由于问题的当前状态与此前所做的陈述完全相反,应该注意的是,现在有一个explicit statement by Brian Goetz关于无序特征的反向传播超过{{1操作被认为是一个错误。 It’s also stated现在认为它根本没有对终端操作的有序性进行反向传播。
还有一个related bug report, JDK-8129120,其状态为“在Java 9中修复”,并且为backported to Java 8, update 60
我使用skip
进行了一些测试,现在的实现似乎确实表现出更直观的行为。
答案 2 :(得分:6)
问题在于您将并行流与forEach一起使用,并且您希望跳过操作依赖于正确的元素顺序,而不是这里的情况。摘自forEach文档:
对于并行流管道,此操作不保证 尊重流的遭遇顺序,因为这样做会牺牲 并行的好处。
我猜基本上发生的事情是跳过操作首先在第二行执行,而不是在第一行执行。如果您使流顺序或使用forEachOrdered,您可以看到它然后产生预期的结果。另一种方法是使用Collectors。
答案 3 :(得分:4)
让我引用一些相关内容 - skip
的Javadoc:
虽然skip()通常是顺序流管道上的廉价操作,但在有序并行流水线上可能非常昂贵,特别是对于大的n值,因为skip(n)被限制为不仅跳过任何n个元素,而且遇到顺序中的前n个元素。
现在,可以确定Files.lines()
定义明确的遭遇顺序并且是ORDERED
流(如果不是,则无法保证均匀在顺序操作中,遇到订单与文件顺序匹配),因此可以保证结果流确定性地仅包含示例中的第二行。
无论是否还有其他内容,保证肯定存在。
答案 4 :(得分:1)
我知道如何解决这个问题,这是我在之前的讨论中看不到的。您可以重新创建将管道拆分为两个管道的流,同时保持整个事物的延迟。
public static <T> Stream<T> recreate(Stream<T> stream) {
return StreamSupport.stream(stream.spliterator(), stream.isParallel())
.onClose(stream::close);
}
public static void main(String[] args) {
recreate(new BufferedReader(new StringReader("JUNK\n1\n2\n3\n4\n5")).lines()
.skip(1).parallel()).forEach(System.out::println);
}
从初始流分割器重新创建流时,您可以有效地创建新管道。在大多数情况下,recreate
将作为no-op
使用,但问题是第一和第二个管道不共享parallel
和unordered
个状态。因此,即使您使用forEach
(或任何其他无序终端操作),也只有第二个流变为无序。
内部非常类似的事情是将您的流与空流连接:
Stream.concat(Stream.empty(),
new BufferedReader(new StringReader("JUNK\n1\n2\n3\n4\n5"))
.lines().skip(1).parallel()).forEach(System.out::println);
虽然它有更多的开销。