LongAccumulator

DoubleAccumulator

class AccumulatorSpec extends Specification { public static final long A = 1 public static final long B = 2 public static final long C = 3 public static final long D = -4 public static final long INITIAL = 0L def '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() == expected where: 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

The order of accumulation within or across threads is not guaranteed and cannot be depended upon, so this class is only applicable to functions for which the order of accumulation does not matter. The supplied accumulator function should be side-effect-free, since it may be re-applied when attempted updates fail due to contention among threads. The function is applied with the current value as its first argument, and the given update as the second argument.





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

AtomicLong

get()

op

LongAccumulator

((I op A) op B) //get()

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

Commutative

((I op A) op (I op B))

((I op B) op (I op A))

op

X op Y = Y op X

X

Y

op

Neutral element (identity)

I

X

op

X op I = I op X = X

I

X

op

Associativity

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

Wrap up

LongAccumulator

DoubleAccumulator

Two classes new in Java 8 deserve some attention:and. They are designed to accumulate (more on what does that mean later) values across threads safely while being extremely fast. A test is worth a thousand words, so here is how it works:So the accumulator takes a binary operator and combines initial value with every accumulated value. That meansequals to. Don't go away yet, there's much more than that. Accumulator can take other operators as well, as illustrated by this use case:Obviously accumulator would work just as well under heavy multi-threaded environment - which it was designed for. Now the question is, what other operations are permitted in(this applies toas well) and why? JavaDoc is not very formal this time (bold mine):In order to understand howworks, what type of operations are permitted and why it's so fast (because it is, compared to e.g), let's start from the back, themethod:Which can be rewritten to not-exactly-equivalent but easier to read:Or even more functionally without internal state:We clearly see that there is some internalarray and that in the end we must go through that array and apply our operator function sequentially on each element. Turns outhas two mechanisms for accumulating values: a singlecounter and an array of values in case of high lock thread contention. Ifis used under no lock contention, only a singlevariable and CAS operations are used, just like in. However if CAS fails, this class falls back to an array of values. You don't want to see the implementation, it's 90 lines long, occasionally with 8 levels of nesting. What you need to know is that it uses simple algorithm to always assign given thread to the same cell (improves cache locality). From now on this thread has its own, almost private copy of counter. It shares this copy with couple of other threads, but not with all of them - they have their own cells. So what you end up in the end is an array of semi-calculated counters which must be aggregated. This is what you saw inmethod.This brings us again to the question, what kind of operators () are permitted in. We know that the same sequence of accumulations under low load will result e.g. in:Which means all values are aggregated in base variable and no counter array is used. However under high load,will split work e.g. into two buckets (cells) and later accumulate buckets as well:or vice-versa:Clearly all invocations ofshould yield the same result, but it all depends on the properties ofoperator being provided (, etc.)We have no control over the order of cells and how they are assigned. That's whyandmust return the same result. More compactly we are looking for such operatorswherefor everyand. This meansmust be commutative Cells are logically initialized with identity (initial) value. We have no control over the number and order of cells, thus the identity value can be applied numerous times in any order. However this is an implementation detail, so it shouldn't affect the result. More precisely, for everyand anyWhich means the identity (initial) valuemust be a neutral value for every argumentto operatorAssume we have the following cells:but the next time they were arranged differentlyKnowing thatis commutative andis a neutral element, we can prove that (for everyand):Which proves thatmust bein order forto actually work.andare highly specialized classes new in JDK 8. JavaDoc is quite vaque but we tried to prove properties that an operator and initial value must fullfil in order for them to do their job. We know that the operator must be associative, commutative and have a neutral element. It would have been so much better if JavaDoc clearly stated that it must be an abelian monoid ;-). Nevertheless for practical purposes these accumulators work only for adding, multiplying, min and max, as these are the only useful operators (with appropriate neutral element) that play well. Subtracting and dividing for example is not associative and commutative, thus can't possibly work. To make matters worse, accumulators would simply behave undeterministically.