Java 8中的两个新类值得关注: LongAccumulator
和DoubleAccumulator
。 它们旨在安全地跨线程安全地累积 (稍后将进一步说明)。 一个测试值一千个单词,所以它是这样工作的:
class AccumulatorSpec extends Specification {public static final long A = 1public static final long B = 2public static final long C = 3public static final long D = -4public static final long INITIAL = 0Ldef 'should add few numbers'() {given:LongAccumulator accumulator = new LongAccumulator({ long x, long y -> x + y }, INITIAL)when:accumulator.accumulate(A)accumulator.accumulate(B)accumulator.accumulate(C)accumulator.accumulate(D)then:accumulator.get() == INITIAL + A + B + C + D}
因此,累加器采用二进制运算符并将初始值与每个累加值组合在一起。 这意味着((((0 + 1) + 2) + 3) + -4)
等于2
。 别走开,还有更多。 累加器也可以采用其他运算符,如以下用例所示:
def 'should accumulate numbers using operator'() {given:LongAccumulator accumulator = new LongAccumulator(operator, initial)when:accumulator.accumulate(A)accumulator.accumulate(B)accumulator.accumulate(C)accumulator.accumulate(D)then:accumulator.get() == expectedwhere:operator | initial || expected{x, y -> x + y} | 0 || A + B + C + D{x, y -> x * y} | 1 || A * B * C * D{x, y -> Math.max(x, y)} | Integer.MIN_VALUE || max(A, B, C, D){x, y -> Math.min(x, y)} | Integer.MAX_VALUE || min(A, B, C, D)
}
显然,累加器在设计用于繁重的多线程环境下也能正常工作。 现在的问题是, LongAccumulator
中允许进行其他哪些操作(这也适用于DoubleAccumulator
),为什么? JavaDoc这次不是很正式(粗体):
不能保证并且不能依赖于线程内或线程间的累加顺序,因此此类仅适用于累加顺序无关紧要的函数。 所提供的累加器功能应无副作用 ,因为当尝试更新由于线程间争用而失败时,可以重新应用该累加器功能 。 应用该函数时,将当前值作为其第一个参数,并将给定的update作为第二个参数。
为了了解LongAccumulator
工作原理,允许哪种类型的操作以及为何如此之快(因为与AtomicLong
相比,它是如此之快),让我们从后面开始get()
方法:
transient volatile long base;
transient volatile Cell[] cells;private final LongBinaryOperator function;public long get() {Cell[] as = cells; Cell a;long result = base;if (as != null) {for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)result = function.applyAsLong(result, a.value);}}return result;
}
可以将其重写为不完全等效,但更易于阅读:
public long get() {long result = base;for (Cell cell : cells)result = function.applyAsLong(result, cell.value);return result;
}
甚至在功能上没有内部状态:
public long get() {return Arrays.stream(cells).map(s -> s.value).reduce(base, function::applyAsLong);
}
我们清楚地看到有一些内部cells
数组,最后我们必须遍历该数组并将操作符函数顺序应用于每个元素。 事实证明, LongAccumulator
具有两种用于累加值的机制:单个base
计数器和在锁线程争用较高的情况下的值数组。 如果在没有锁争用的情况下使用LongAccumulator
,则仅使用单个volatile base
变量和CAS操作,就像在AtomicLong
。 但是,如果CAS失败,则此类退回到一组值。 您不想看到实现,它有90行,有时会有8个嵌套层。 您需要知道的是,它使用简单的算法始终将给定线程分配给同一单元(提高了缓存的局部性)。 从现在开始,该线程具有其自己的,几乎是私有的计数器副本。 它与其他几个线程共享此副本,但并非与所有其他线程共享-它们具有自己的单元。 因此,最终您得到的是必须汇总的半计算计数器数组。 这就是您在get()
方法中看到的。
这又使我们想到一个问题, LongAccumulator
中允许使用LongAccumulator
op
符( op
)。 我们知道在低负载下相同的累积顺序将导致:
((I op A) op B) //get()
这意味着所有值都聚集在基本变量中,并且不使用任何计数器数组。 但是,在高负载下, LongAccumulator
会将工作分解为两个铲斗(单元),然后LongAccumulator
铲斗也进行蓄积:
(I op A) //cell 1
(I op B) //cell 2(I op A) op (I op B) //get()
或相反亦然:
(I op B) //cell 1
(I op A) //cell 2(I op B) op (I op A) //get()
显然,所有对get()
调用都应产生相同的结果,但这都取决于所提供的op
符的属性( +
, *
, max
等)。
可交换的
我们无法控制单元的顺序及其分配方式。 这就是((I op A) op (I op B))
和((I op B) op (I op A))
必须返回相同结果的原因。 更紧凑地说,我们正在寻找这样的运算符op
,其中每个X
和Y
X op Y = Y op X
这意味着op
必须是可交换的 。
中性元素(身份)
单元格使用标识(初始)值I
进行逻辑初始化。 我们无法控制单元的数量和顺序,因此身份值可以按任意顺序多次应用。 但这是一个实现细节,因此不应影响结果。 更确切地说,对于每个X
和任何op
:
X op I = I op X = X
这意味着对于运算符op
每个参数X
,标识(初始)值I
必须为中性值。
关联性
假设我们有以下单元格:
I op A // cell 1
I op B // cell 2
I op C // cell 3
((I op A) op (I op B)) op (I op C) //get()
但是下一次他们的布置有所不同
I op C // cell 1
I op B // cell 2
I op A // cell 2
((I op C) op (I op B)) op (I op A) //get()
知道op
是可交换的,而I
是中性元素,我们可以证明(对于每个A
, B
和C
):
((I op A) op (I op B)) op (I op C) = ((I op C) op (I op B)) op (I op A)
(A op B) op C = (C op B) op A
(A op B) op C = A op (B op C)
这证明op
必须具有关联性 , LongAccumulator
才能真正起作用。
包起来
LongAccumulator
和DoubleAccumulator
是JDK 8中新增的高度专业化的类。JavaDoc相当荒谬,但是我们尝试证明操作员和初始值必须满足的属性才能使它们完成工作。 我们知道运算符必须是关联的 , 可交换的并且具有中性元素。 如果JavaDoc明确声明它必须是阿贝尔的类半体动物 ;-),那就更好了。 然而,出于实际目的,这些累加器仅用于加,乘,最小和最大值,因为它们是唯一发挥良好作用的有用运算符(带有适当的中性元素)。 例如,减法和除法不是关联和可交换的,因此不可能工作。 更糟的是,累加器只会表现得不确定。
翻译自: https://www.javacodegeeks.com/2015/06/how-longaccumulator-and-doubleaccumulator-classes-work.html