在尝试学习Java lambdas时,我遇到了一篇文章(下面列出),在一篇关于流API限制的章节中,他指出:“有状态lambda在顺序执行时通常不是问题,但是当流执行是并行化的,它打破了“。然后,他将此代码作为执行顺序导致的问题示例:
List<String> ss = ...;
List<String> result = ...;
Stream<String> stream = ss.stream();
stream.map(s -> {
synchronized (result) {
if (result.size() < 10) {
result.add(s);
}
}
})
.forEach(e -> { });
我可以看到如果它是并行化的,这将是非确定性的,但是我看不出你将如何用无状态lambda来解决这个问题 - 是不是存在一些关于向东西添加东西的固有的非确定性的东西以平行的方式列出。一个六岁的帽子可以理解的例子,也许在C#中,将非常感激。
链接到原始文章http://blog.hartveld.com/2013/03/jdk-8-33-stream-api.html
答案 0 :(得分:9)
我知道你在暗示你的问题,我会尽力解释。
考虑一个由8个元素组成的输入列表:
[1, 2, 3, 4, 5, 6, 7, 8]
并假设流将以下列方式并行化,实际上它们没有,并行的确切过程很难理解。
但是现在,假设他们将大小除以2,直到剩下两个元素。
分支部门看起来像这样:
第一师:
[1, 2, 3, 4]
[5, 6, 7, 8]
第二师:
[1, 2]
[3, 4]
[5, 6]
[7, 8]
现在我们有四个块(在我们的理论中)将由四个不同的线程处理,这些线程彼此不了解。
这确实可以通过同步一些外部资源来解决,但是你失去了并行化的好处,所以我们需要假设我们不同步,当我们不同步时,其他线程将看不到任何其他线程有什么完了,所以我们的结果将是垃圾。
现在问你关于无状态的问题的一部分,然后如何才能正确地并行处理?如何以正确的顺序将以并行处理的元素添加到列表中?
首先假设一个简单的映射函数,您使用lambda i -> i + 10
进行映射,然后在foreach中使用System.out::println
进行打印。
现在在第二次分裂后,将发生以下情况:
[1, 2] -> [11, 12] -> { System.out.println(11); System.println(12); }
[3, 4] -> [13, 14] -> { System.out.println(13); System.println(14); }
[5, 6] -> [15, 16] -> { System.out.println(15); System.println(16); }
[7, 8] -> [17, 18] -> { System.out.println(17); System.println(18); }
除了按顺序处理由同一线程处理的所有元素(内部状态,不依赖)之外,订单无法保证。
如果你想按顺序处理它们,那么你需要使用forEachOrdered
,这将确保所有线程以正确的顺序运行,并且你不会失去过多的并行效益,因为这样做它仅适用于最终状态。
要了解如何将项目parelllized添加到列表中,请使用Collectors.toList()
来查看此内容,该[1, 2]
提供以下方法:
现在,第二次分裂后会发生以下情况:
对于每四个线程,它将执行以下操作(此处仅显示一个线程):
[11, 12]
。List<Integer>
。11
。12
添加到列表中。[11, 12] ++ [13, 14] = [11, 12, 13, 14]
添加到列表中。现在所有线程都已完成此操作,我们有四个包含两个元素的列表。
现在,以指定的顺序进行以下合并:
[15, 16] ++ [17, 18] = [15, 16, 17, 18]
[11, 12, 13, 14] ++ [15, 16, 17, 18] = [11, 12, 13, 14, 15, 16, 17, 18]
因此结果列表是有序的,并且映射已经并行完成。现在你也应该能够看到为什么parallallization需要更高的最小值只作为两个项目,否则新列表的创建和合并变得太昂贵。
我希望您现在明白为什么流操作应该无状态以获得并行化的全部好处。
答案 1 :(得分:0)
减少这个问题,看起来它只是在流中找到前10个元素。并分别对整个流进行预测。 s.limit(10).collect(...);
和s.forEach(...);
。地图调用实际上并没有返回任何东西,所以我怀疑这个编译。
答案 2 :(得分:0)
这是来自@skiwi的一个很好的例子,让我看看我是否可以添加一点。
并行计算中的“有序”一词通常意味着以与顺序过程相同的顺序返回结果。也就是说,调用sequential.method()或parallel.method(),结果看起来是一样的。
withforEachOrdered()的问题在于框架无法为每个任务的结果创建唯一对象,并且在完成时对它们进行排序而不会停顿。因此,它将流视为平衡树。该框架创建一个具有父/子关联的ConcurrentHashMap。它首先执行左子项,然后执行右子项,然后父项强制执行before-before关系,其中处理应该是并发的。从有序结果到有序顺序处理。
您需要做的是订购结果,而不是按照遭遇顺序处理。为每个最终分区创建包含数组部分的对象(这里我们使用@skiwi的第二分区),计算填写的处理结果和每个对象的序列号。让线程同时处理对象。完成所有线程后,按序列号排序对象并完成工作。