Java 如何使stream reduce成为线程安全的?
Java stream API提供了一种通用的Java 如何使stream reduce成为线程安全的?,java,multithreading,java-stream,Java,Multithreading,Java Stream,Java stream API提供了一种通用的.reduce(标识、累加器)方法 从javadocs中可以清楚地看到,累加器应该是一个无状态函数 但是,我有一个关于标识对象的问题,即它应该是线程安全的吗 假设identity是一个java对象,而累加器以一种非原子的方式修改该对象,例如累加器查看标识的状态,然后决定如何准确修改其内部状态。显然,可能会同时运行多个reduce操作。在这种情况下,出现了几个问题: 这个reduce操作是否应该是identity对象范围内的原子操作 仅仅使ident
.reduce(标识、累加器)
方法
从javadocs中可以清楚地看到,累加器应该是一个无状态函数
但是,我有一个关于标识对象的问题,即它应该是线程安全的吗
假设identity
是一个java对象,而累加器以一种非原子的方式修改该对象,例如累加器
查看标识的
状态,然后决定如何准确修改其内部状态。显然,可能会同时运行多个reduce操作。在这种情况下,出现了几个问题:
- 这个reduce操作是否应该是
identity
对象范围内的原子操作
- 仅仅使
identity
对象不可变并在每次reduce时返回一个新实例就足够了吗
通常,acculator
是一个英语单词,意思是:“如果你想要并行,你就完全被套住了”。它就在这个词里:积累——随着时间的推移而积累。除了从头开始,积累直到完成,没有办法把它做好
但是,java通过添加两个需求来解决这个问题:
结合性a X(b X c)
必须产生与(a X b)X c
相同的结果,其中X是累加器函数
身份功能ident X a
必须等于a
,其中ident
是传递给reduce
的标识,X是累加器函数
让我们以函数(a,b)->a+b
和身份0
为例,如果您的目的是对列表求和,则该函数满足这两个要求
Java可以通过对任意项求和,然后对这些项的结果求和来实现并行化<代码>[1,5,9,12]
可以通过先将列表一分为二,然后将这两个子列表分别交给线程进行求和,然后对每个线程提供的答案求和来求和这意味着java将在流中的任意点开始多次累积,并将在任意点多次应用标识作为其累积的一部分,如果您的标识对象本身是可变的,这将带来快速的问题
基本上没有办法将可变的identity
对象的概念与java的reduce
函数结合起来。从根本上说,它不是为了这样工作而设计的
与求和示例相反:在(a,b)->a+b
累加器中,不修改a和b,而是修改a和b;相反,它们被组合成新创建的第三个值,这就是您应该如何使用此方法
与某些其他语言中的foldLeft
不同,这些语言既不要求acgregatorfunction(ident,A)
等于A,也不要求关联性,但根据定义根本无法将其并行化。该foldLeft可用于可变状态。例如,这里有一个伪代码中使用foldLeft求和的例子:(注意,newint[1]
在这里用作可变整数):
这个概念(累加器函数的LHS始终是相同的东西,即您的标识对象,在您沿着流移动时被修改为集成流中的每个值)与java的reduce不兼容,并且据我所知,java没有(简单的)方法对流执行这种操作
因此:情况更糟“线程安全”还不够好,它需要是不可变的。一旦它是不可变的,它就是线程安全的
仅仅使identity对象不可变并在每次reduce时返回一个新实例就足够了吗
这不仅仅是“足够好”,这或多或少是使用reduce
的唯一明智的方法。文档中包括了这一点,但不是直接暗示的
标识值必须是累加器函数的标识这意味着对于所有t,累加器.apply(标识,t)等于t
一旦修改了identity
,就像您所说的,即使是以线程安全的方式,也违反了上述规则;因此,无法保证预期结果
对于第二个问题,答案稍微复杂一些。只要没有人滥用身份(通过修改其内部状态),就不必使身份不可变。当然,使其不可变在这方面会有很大帮助。考虑将重载与附加的组合器一起使用。如果您希望始终是线程安全的,那么您可以使用多argscollect()
或使用Collector.Of()
调用单args方法(甚至您自己的Collector
实现). 这样,您可以拥有多个“标识”,它们最终以线程安全的方式合并在一起;另一种方式是始终返回一个新标识。即,创建一个新实例,复制旧值,修改新实例的状态并返回所述实例。如果累加器修改标识
,则标识
是累加器
状态的一部分。因此,累加器
不是无状态的。累加器
不应修改其任何传入参数。这也解决了累加器不知道其参数是否或哪些参数是标识值的问题。在foldLeft上下文中不应使用术语“标识对象”。除此之外,collect(()->new int[1],(a,b)->a[0]+=b,(a1,a2)->a1[0]+=a2[0])[0]
并没有那么难…@rzwitserroot-术语assumulator对我来说是新的,但我认为这是一个有趣的创新哦,这是我灵感更大的打字错误之一:)谢谢你让我知道,我会编辑这个答案。你以前的答案之一可能会
int sum = stream.foldLeft(new int[1], (int[] a, int b) -> a[0] += b)[0];