Java 并行流、收集器和线程安全

Java 并行流、收集器和线程安全,java,concurrency,parallel-processing,java-8,java-stream,Java,Concurrency,Parallel Processing,Java 8,Java Stream,请参阅下面的简单示例,该示例统计列表中每个单词的出现次数: Stream<String> words = Stream.of("a", "b", "a", "c"); Map<String, Integer> wordsCount = words.collect(toMap(s -> s, s -> 1, (i, j) -> i + j)); 但

请参阅下面的简单示例,该示例统计列表中每个单词的出现次数:

Stream<String> words = Stream.of("a", "b", "a", "c");
Map<String, Integer> wordsCount = words.collect(toMap(s -> s, s -> 1,
                                                      (i, j) -> i + j));
但是我注意到,
wordscont
是一个简单的
HashMap
,因此我想知道是否需要显式请求并发映射以确保线程安全:

Map<String, Integer> wordsCount = words.parallel()
                                       .collect(toConcurrentMap(s -> s, s -> 1,
                                                                (i, j) -> i + j));
Map wordscont=words.parallel()
.collect(收集)到当前地图(s->s,s->1,
(i,j)->i+j);
非并发收集器可以安全地与并行流一起使用吗?还是我应该仅在从并行流进行收集时使用并发版本?

非并发收集器可以安全地与并行流一起使用吗?或者我应该仅在从并行流进行收集时使用并发版本吗

在并行流的
collect
操作中使用非并发收集器是安全的

收集器的
界面中,在包含六个项目符号的部分中,是这样的:

对于非并发收集器,从结果提供者、累加器或组合器函数返回的任何结果都必须是串行线程限制的。这使得收集可以并行进行,而无需收集器实现任何额外的同步。缩减实现必须管理输入被正确分区,分区被隔离处理,并且只有在累积完成后才进行合并

这意味着
收集器
类提供的各种实现可以与并行流一起使用,即使其中一些实现可能不是并发收集器。这也适用于您自己可能实现的任何非并发收集器。它们可以安全地与并行流一起使用,前提是您的收集器不干扰流源、无副作用、与顺序无关等

我还建议您阅读java.util.stream包文档的一节。在本节的中间是一个示例,它被称为可并行化,但它将结果收集到<>代码> ARARYList,这不是线程安全的。
其工作方式是,以非并发收集器结尾的并行流确保不同的线程始终在中间结果集合的不同实例上运行。这就是为什么收集器有一个
Supplier
函数,用于创建尽可能多的中间集合,因为有线程,所以每个线程都可以累积到自己的集合中。当要合并中间结果时,它们会在线程之间安全地传递,并且在任何给定时间,只有一个线程正在合并任何一对中间结果。

将非并发集合和非原子计数器用于并行流是安全的

如果您查看的文档,您会发现以下段落:

reduce(Object,BinaryOperator)
类似,collect操作可以并行化,而不需要额外的同步

对于方法:

虽然与在循环中简单地改变运行总数相比,这似乎是一种更为迂回的执行聚合的方法,但缩减操作的并行化更为优雅,不需要额外的同步,并且大大降低了数据争用的风险

这可能有点令人惊讶。但是,请注意,并行流是基于fork-join模型的。这意味着并行执行的工作方式如下:

  • 将序列拆分为大小大致相同的两部分
  • 单独处理每个零件
  • 收集两个部分的结果,并将它们合并为一个结果
在第二步中,这三个步骤递归地应用于子序列

一个例子应该说明这一点。

IntStream.range(0, 4)
    .parallel()
    .collect(Trace::new, Trace::accumulate, Trace::combine);
类跟踪的唯一目的是记录构造函数和方法调用。如果执行此语句,它将打印以下行:

thread:  9  /  operation: new
thread: 10  /  operation: new
thread: 10  /  operation: accumulate
thread:  1  /  operation: new
thread:  1  /  operation: accumulate
thread:  1  /  operation: combine
thread: 11  /  operation: new
thread: 11  /  operation: accumulate
thread:  9  /  operation: accumulate
thread:  9  /  operation: combine
thread:  9  /  operation: combine

您可以看到,已经创建了四个跟踪对象,在每个对象上调用了一次“累积”,并且使用了三次“合并”将四个对象合并为一个。每个对象一次只能由一个线程访问。这使得代码线程安全,同样适用于方法收集器::toMap。

如果遵循规范中的规则,则所有收集器都可以安全地并行或顺序运行。并行准备是此处设计的关键部分

并发和非并发收集器之间的区别与并行化方法有关

普通(非并发)收集器通过合并子结果进行操作。因此,源被划分成一组块,每个块被收集到一个结果容器(如列表或映射),然后子结果被合并到一个更大的结果容器中。这是安全且保持顺序的,但对于某些类型的容器,尤其是映射,可能会很昂贵,因为按键合并两个映射通常会很昂贵

相反,并发收集器创建一个结果容器,该容器的插入操作保证是线程安全的,并从多个线程向其中爆破元素。对于像ConcurrentHashMap这样的高度并发的结果容器,这种方法可能比合并普通的HashMap执行得更好


因此,并发收集器是对普通收集器的严格优化。他们也不是免费的;因为元素是从多个线程中被炸入的,所以并发收集器通常无法保持遭遇顺序。(但是,通常你并不在意——在创建字数统计直方图时,你并不在意你首先统计的是“foo”的哪个实例。)

顶部答案的最后一段似乎描述了你的第(3)段。你是说这是错误的,实际上是(4和5)那样做的?@Noume
thread:  9  /  operation: new
thread: 10  /  operation: new
thread: 10  /  operation: accumulate
thread:  1  /  operation: new
thread:  1  /  operation: accumulate
thread:  1  /  operation: combine
thread: 11  /  operation: new
thread: 11  /  operation: accumulate
thread:  9  /  operation: accumulate
thread:  9  /  operation: combine
thread:  9  /  operation: combine