为什么我应该在并行流中使用并发特征和收集?

为什么我应该在并行流中使用并发特征和收集:

List<Integer> list =
        Collections.synchronizedList(new ArrayList<>(Arrays.asList(1, 2, 4)));

Map<Integer, Integer> collect = list.stream().parallel()
        .collect(Collectors.toConcurrentMap(k -> k, v -> v, (c, c2) -> c + c2));

而不是:

Map<Integer, Integer> collect = list.stream().parallel()
        .collect(Collectors.toMap(k -> k, v -> v, (c, c2) -> c + c2));

换句话说,不使用这个特性有什么副作用,它对内部流操作有用吗?


答案 1

这两个收集器以根本不同的方式运行。

首先,Stream 框架将工作负荷拆分为可以并行处理的独立块(这就是为什么您不需要特殊集合作为源的原因)。synchronizedList

使用非并发收集器,将使用收集器的供应商创建一个本地容器(此处为 a )并将其累积到本地容器中(放置条目),从而处理每个块。必须合并这些部分结果,即一张地图已放入另一张地图中,以获得最终结果。Map

并发收集器支持并发累积,因此只会创建一个,并且所有线程同时累积到该映射中。因此,完成后,不需要合并步骤,因为只有一个地图。ConcurrentMap


因此,这两个收集器都是线程安全的,但根据任务的不同,它们可能表现出完全不同的性能特征。如果 Stream 在收集结果之前的工作负载很重,则差异可以忽略不计。如果像您的示例中一样,在 collect 操作之前没有相关的工作,则结果很大程度上取决于必须合并映射的频率,即相同的键发生,以及实际目标如何处理并发情况下的争用。ConcurrentMap

如果大多数情况下具有不同的键,则非并发收集器的合并步骤可能与上一个放置一样昂贵,从而破坏了并行处理的任何好处。但是,如果存在大量重复的键,需要合并值,则对同一键的争用可能会降低并发收集器的性能。

因此,没有简单的“哪个更好”的答案(好吧,如果有这样的答案,为什么要费心添加另一个变体)。这取决于您的实际操作。您可以使用预期的方案作为选择一个方案的起点,但此时应使用实际数据进行度量。由于两者是等效的,因此您可以随时更改您的选择。


答案 2

首先,我给霍尔格的答案打了+1,这是一个很好的答案。我会试着简单地说一下,说:

CONCURRENT ->多个线程以不特定顺序在同一容器上抛出数据(ConcurrentHashMap)

非并发 ->多个线程合并其中间结果。

理解它的最简单方法(恕我直言)是编写一个自定义收集器并使用它的每种方法:供应商,累加器,组合器。

这里已经涵盖了这一


推荐