如何让流减少成为线程安全?

2022-09-03 02:00:24

Java 流 API 提供了一种通用方法。.reduce(identity, accumulator)

从javadocs中可以很清楚地看出,累加器应该是一个无状态函数。

但是,我对该对象有疑问,即它应该是线程安全的吗?identity

假设 an 是一个 java 对象,而 an 以一种非原子的方式修改该对象,例如 查看状态,然后决定如何精确地修改其内部状态。显然,可能会同时运行多个 reduce 操作。在这种情况下,会出现几个问题:identityaccumulatoraccumulatoridentity's

  • 这个 reduce 操作是否应该在对象的范围内是原子的?identity
  • 仅仅使对象不可变并在每次减少时返回一个新实例就足够了吗?identity

答案 1

通常,这是一个英语单词,意思是:“如果你想要并行性,你就完全被软管化了”。它就在这个词中:积累 - 随着时间的推移而聚集。除了从头开始,并应用积累直到完成之外,没有办法把它做好。accumulator

但是,java通过添加2个要求来解决这个问题:

  1. 关联性。 必须产生与 相同的结果,其中 X 是累加器函数。a X (b X c)(a X b) X c
  2. 标识函数。 必须等于 ,其中 是传递给的恒等式,X 是累加器函数。ident X aaidentreduce

让我们以函数和标识为例,如果您的意图是求和列表,则满足这两个要求。(a, b) -> a + b0

Java可以通过对任意项求和,然后对这些项的结果求和来并行化这一点。 可以通过以下方式求和:首先将列表分成两部分,然后将这两个子列表交给线程进行单独求和,然后对每个线程提供的答案求和。这意味着java将在流中的任意点开始多次累积,并将在任意点上应用标识作为其累积的一部分,并且如果您的标识对象本身是可变的,这将带来快速的问题。[1, 5, 9, 12]

基本上没有办法将可变对象的概念和java的功能结合起来。从根本上说,它的设计目的不是以这种方式工作的。identityreduce

与总和示例相反:不是在累加器中修改 a,而是既不修改 a 也不修改 b;相反,它们被组合成新创建的第三个值,这就是你应该如何使用这种方法。(a, b) -> a + b

与某些其他语言相反,其他语言既不要求等于A,也不要求关联性,但根据定义,根本无法并行化它。foldLeft可以与可变状态一起使用。例如,下面是在伪代码中使用 foldLeft 求和的 impl:(请注意,此处用作可变整数):foldLeftaccumulatorFunction(ident, A)new int[1]

int sum = stream.foldLeft(new int[1], (int[] a, int b) -> a[0] += b)[0];

这个概念(你的累加器函数的LHS总是一样的,即你的身份对象,当你沿着它移动时,它被修改以集成流中的每个值)与java的reduce不兼容,据我所知,java没有(简单的)方法来对流做这种事情。

因此:情况更糟!“线程安全”还不够好,它需要是不可变的。一旦它是不可变的,它就是线程安全的。

仅仅使标识对象不可变并在每次减少时返回一个新实例就足够了吗?

这不仅仅是“足够好”,这或多或少是唯一合理的使用方式。reduce


答案 2

这在文档中有所涉及,但不是直接的,它是隐含的。

标识值必须是累加器函数的标识。这意味着对于所有 t,accumulator.apply(identity, t) 等于 t

一旦被修改,就像你说的,即使以线程安全的方式,上面的规则也被违反了;因此不能保证预期的结果。identity

对于第二个问题,答案稍微复杂一些。您不必使不可变,只要没有人滥用它(通过修改其内部状态)。当然,在这方面,制作它有很大帮助。identityimmutable