Java 这是Files.lines()中的一个bug,还是我对并行流有误解?
环境:Ubuntu x86_64(14.10),Oracle JDK 1.8u25 我尝试使用一个并行的数据流,但我想使用第一行(它是一个带有头的CSV文件)。因此,我尝试这样做: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
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);