Java 这是Files.lines()中的一个bug,还是我对并行流有误解?

Java 这是Files.lines()中的一个bug,还是我对并行流有误解?,java,parallel-processing,java-8,java-stream,Java,Parallel Processing,Java 8,Java Stream,环境:Ubuntu x86_64(14.10),Oracle JDK 1.8u25 我尝试使用一个并行的数据流,但我想使用第一行(它是一个带有头的CSV文件)。因此,我尝试这样做: try ( final Stream<String> stream = Files.lines(thePath, StandardCharsets.UTF_8) .skip(1L).parallel(); ) { // etc } 代码也同样简单: public stat

环境:Ubuntu x86_64(14.10),Oracle JDK 1.8u25

我尝试使用一个并行的数据流,但我想使用第一行(它是一个带有头的CSV文件)。因此,我尝试这样做:

try (
    final Stream<String> stream = Files.lines(thePath, StandardCharsets.UTF_8)
        .skip(1L).parallel();
) {
    // etc
}
代码也同样简单:

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次):

我错过了什么


编辑问题或误解似乎远不止于此(以下两个例子是FreeNode的##java上的一位同事捏造的):

@霍尔格建议,对于任何
有序
且不
大小
的流,都会发生这种情况

Stream.of("Hello", "World")
    .filter(x -> true)
    .parallel()
    .skip(1L)
    .forEach(System.out::println);

此外,它源于所有已经发生的讨论,即问题(如果是一个?)是与
.forEach()
(as)。

问题是您正在与forEach一起使用并行流,并且您希望跳过操作依赖于正确的元素顺序,而这里的情况并非如此。文件摘录:

对于平行流管道,此操作不保证 尊重溪流的遭遇顺序,因为这样做会牺牲生命 并行性的好处


我猜基本上是跳过操作首先在第二行执行,而不是第一行。如果将流设置为连续或使用,则可以看到它会产生预期的结果。另一种方法是使用。

让我引用一些与
skip的Javadoc相关的内容:

虽然skip()通常是顺序流管道上的一种廉价操作,但在有序并行管道上,它可能非常昂贵,尤其是对于大值n,因为skip(n)被约束为不仅跳过任何n个元素,而且跳过相遇顺序中的前n个元素

现在,可以肯定的是,
Files.lines()
有一个定义良好的遭遇顺序,并且是一个
有序的
流(如果不是,即使在顺序操作中也无法保证遭遇顺序与文件顺序匹配),因此,可以保证生成的流将决定性地仅包含示例中的第二行


无论这是否有其他原因,保证是肯定的。

这个答案已经过时了-请阅读


快速回答以下问题:观察到的行为是有意的没有bug,所有这些都是根据文档进行的。但可以说,这种行为应该得到更好的记录和沟通。应该更清楚地看到,
forEach
如何忽略排序

我将首先介绍允许观察到的行为的概念。这为剖析问题中给出的一个示例提供了背景。我将在高级上执行此操作,然后在非常低级上再次执行此操作

[TL;DR:单独阅读,高层次的解释将给出一个粗略的答案。]

概念 与其讨论
Stream
s,它是由流相关方法操作或返回的类型,不如讨论流操作和流管道。方法调用
line
skip
parallel
是流操作,它们构建流管道[1],并且正如其他人所指出的,当调用终端操作
forEach
时,管道作为一个整体进行处理[2]

管道可以被认为是一系列操作,这些操作一个接一个地在整个流上执行(例如,过滤所有元素,将剩余元素映射到数字,对所有数字求和)但这是误导一个更好的比喻是,终端操作通过每个操作提取单个元素[3](例如,获取下一个未过滤的元素,映射它,将它添加到sum,请求下一个元素)。一些中间操作可能需要遍历多个(例如
跳过
)甚至所有(例如
排序
)元素,然后才能返回请求的下一个元素,这是操作中状态的来源之一

每个操作都用以下符号表示其特性:

  • DISTINCT
  • 已排序
  • 已订购
  • size
  • 短路
它们跨流源、中间操作和终端操作组合,构成管道的特征(作为一个整体),然后用于优化[4]。类似地,管道是否并行执行是整个管道的属性[5]

因此,无论何时对这些特性进行假设,都必须仔细查看构建管道的所有操作,而不管它们的应用顺序,以及它们的保证是什么。执行此操作时,请记住终端操作是如何通过管道拉动每个单独的元素的

例子 让我们看看这个特殊情况:

BufferedReader fooBarReader = new BufferedReader(new StringReader("Foo\nBar"));
fooBarReader.lines()
        .skip(1L)
        .parallel()
        .forEach(System.out::println);
高层 不管您的流源是否已排序(它是),通过调用
forEach
(而不是
forEachOrdered
),您可以声明顺序对您来说无关紧要,[6],这有效地将
跳过
从“跳过前n个元素”减少为“跳过任意n个元素”[7](因为没有秩序,前者变得毫无意义)

因此,如果顺序保证了加速,那么您就赋予管道忽略顺序的权利。对于并行执行,它显然是这么认为的,这就是为什么您会得到观察到的输出。因此您观察到的是预期的行为,并且没有bug

请注意,此
skip
的状态不冲突
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
Stream.of("Hello", "World")
    .filter(x -> true)
    .parallel()
    .skip(1L)
    .forEach(System.out::println);
BufferedReader fooBarReader = new BufferedReader(new StringReader("Foo\nBar"));
fooBarReader.lines()
        .skip(1L)
        .parallel()
        .forEach(System.out::println);
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);
}
Stream.concat(Stream.empty(), 
    new BufferedReader(new StringReader("JUNK\n1\n2\n3\n4\n5"))
          .lines().skip(1).parallel()).forEach(System.out::println);