如何使流减少线程安全?
Java 流 API 提供了一种通用.reduce(identity, accumulator)方法。
从 javadocs 中可以清楚地看出累加器应该是一个无状态函数。
但是,我有一个关于identity对象的问题,即它应该是线程安全的吗?
假设 anidentity是一个 java 对象,并且 anaccumulator以修改不是原子的方式修改该对象,例如accumulator查看identity's状态,然后决定如何准确地修改它的内部状态。很明显,可能会同时运行多个 reduce 操作。在这种情况下,会出现几个问题:
- 这个减少操作应该在
identity对象范围内是原子的吗? - 仅仅让
identity对象不可变并在每次减少时返回一个新实例就足够了吗?
回答
通常,accumulator是一个英文单词,意思是:“如果你想要并行性,你就完蛋了”。就在这个词中:积累 - 随着时间的推移聚集。没有办法做对,只能从头开始,然后积累直到完成。
但是,java 通过添加 2 个要求来解决这个问题:
- 关联性。
a X (b X c)必须产生与 相同的结果(a X b) X c,其中 X 是累加器函数。 - 身份功能。
ident X a必须等于a,其中ident是您传递给的身份reduce,X 是累加器函数。
让我们以 function(a, b) -> a + b和 identity为例0,如果您的意图是对列表求和,它可以满足这两个要求。
Java 可以通过对任意项求和然后对这些项的结果求和来并行化这一点。[1, 5, 9, 12]可以通过首先将列表一分为二,然后将这两个子列表交给线程单独求和,然后对每个线程提供的答案求和来求和。这意味着 java 将在流中的任意点多次开始累积,并将在任意点任意次数应用身份作为其累积的一部分,如果您的身份对象本身是可变的,这会带来迅速的问题。
基本上没有办法将可变identity对象的概念和 java 的reduce函数结合起来。它从根本上不是为了那样工作而设计的。
对比 sum 例子:不是修改(a, b) -> a + b累加器中的 a,a 和 b 都没有被修改;相反,它们被组合成一个新创建的第三个值,这就是您应该如何使用此方法。
与foldLeft某些其他语言形成对比,这些语言既不需要accumulatorFunction(ident, A)等于 A,也不需要关联性,但根据定义,它根本无法并行化。该 foldLeft可用于可变状态。例如,这是一个使用 foldLeft 进行求和的实现,在伪代码中:(注意new int[1]这里用作可变整数):
int sum = stream.foldLeft(new int[1], (int[] a, int b) -> a[0] += b)[0];
这个概念(你的累加器函数的 LHS 总是相同的东西,即你的身份对象,在你沿着它移动时被修改以整合流中的每个值)与 java 的 reduce不兼容,并且据我所知回想一下,java 没有(简单的)方法可以对流做这种事情。
因此:情况更糟!“线程安全”还不够好,它需要是不可变的。一旦它是不可变的,它就是线程安全的。
仅仅使标识对象不可变并在每次减少时返回一个新实例就足够了吗?
这不仅仅是“足够好”,这或多或少是使用reduce.
- You should not use the term “identity object” in the context of foldLeft. Besides that, `collect(() -> new int[1], (a,b) -> a[0] += b, (a1,a2) -> a1[0] += a2[0])[0]` is not so much harder…
回答
这包含在文档中,但不是直接的,它是隐含的。
标识值必须是累加器函数的标识。这意味着对于所有 t, accumulator.apply(identity, t) 等于 t。
一旦identity被修改,就像你说的那样,即使以线程安全的方式,也违反了上述规则;因此不能保证预期的结果。
对于第二个问题,答案稍微复杂一些。你不具备让identity一成不变的,只要没人的残害,是(通过修改其内部状态)。当然,immutable在这方面制作它有很大帮助。
- One of your earlier answers that could relate and explain with example https://stackoverflow.com/questions/45054372/what-do-the-stream-reduce-requirements-exactly-entail