43 Differences Between Adder and Accumulator in Java 8

43 Differences Between Adder and Accumulator in Java 8 #

This lesson mainly introduces the difference between Adder and Accumulator in Java 8.

Introduction to Adder #

We need to know that Adder and Accumulator are both introduced in Java 8 and are relatively new classes. Regarding Adder, for example, the most typical one is LongAdder, which we have already explained in Lesson 40. In high-concurrency scenarios, LongAdder is more efficient than AtomicLong because AtomicLong is only suitable for low-concurrency scenarios. In high-concurrency scenarios, the high conflict probability of CAS will frequently cause spinning, affecting overall efficiency.

In contrast, LongAdder introduces the concept of segmented locks. When the competition is not intense, all threads modify the same Base variable through CAS. However, when the competition is intense, LongAdder assigns different threads to different Cells for modification, reducing the probability of conflicts and improving concurrency.

Introduction to Accumulator #

So what does Accumulator do? Accumulator is very similar to Adder. In fact, Accumulator is a more general version of Adder. For example, LongAccumulator is an enhanced version of LongAdder because LongAdder’s API only supports addition and subtraction of numerical values, while LongAccumulator provides custom function operations.

Some students may still not fully understand after my explanation, so let’s use a very intuitive code example to illustrate. The code is as follows:

public class LongAccumulatorDemo {

    public static void main(String[] args) throws InterruptedException {

        LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0);

        ExecutorService executor = Executors.newFixedThreadPool(8);

        IntStream.range(1, 10).forEach(i -> executor.submit(() -> accumulator.accumulate(i)));

        Thread.sleep(2000);

        System.out.println(accumulator.getThenReset());

    }

}

In this code:

  • First, a LongAccumulator is created, and two parameters are passed to it;
  • Then, an 8-thread thread pool is created, and 9 tasks from 1 to 9 are submitted to the thread pool using the integer stream IntStream;
  • After that, a two-second delay is applied, which waits for the tasks in the thread pool to finish executing;
  • Finally, the value of the accumulator is printed.

The result of running this code is 45, which represents the result of 0+1+2+3+…+8+9=45. How should we interpret this result? Let’s focus on this line when creating the LongAccumulator:

LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0);

In this statement, we pass in two parameters: the first parameter of the LongAccumulator constructor is the binary expression, and the second parameter is the initial value of x, which is 0. In the binary expression, x is the result of the previous calculation (except for the first time when it needs to be passed in), and y is the newly passed value for this calculation.

Case analysis #

Let’s take a look at the execution process of the above code. When executing accumulator.accumulate(1), we first need to know what x and y are at this time. In the first execution, x is the second parameter in the LongAccumulator constructor, which is 0, and the value of y for the first execution is the 1 passed in by the accumulator.accumulate(1) method. Then, according to the expression x+y, we calculate 0+1=1, and this result will be assigned to x for the next calculation. The y value for the next calculation is 2 passed in by accumulator.accumulate(2), so the result of the next calculation is 1+2=3.

In the statement IntStream.range(1, 10).forEach(i -> executor.submit(() -> accumulator.accumulate(i))), we actually use the integer stream to submit 9 tasks from 1 to 9 to the thread pool. It is equivalent to executing:

accumulator.accumulate(1);

accumulator.accumulate(2);
accumulator.accumulate(3);

...

accumulator.accumulate(8);

accumulator.accumulate(9);

According to the deduction above, we can deduce its internal operation, which means that LongAccumulator performs the following steps:

0+1=1;

1+2=3;

3+3=6;

6+4=10;

10+5=15;

15+6=21;

21+7=28;

28+8=36;

36+9=45;

It should be noted that the order of addition here is not fixed, and it is not necessarily in order of 1, 2, 3, etc. It can vary, such as adding 5 first, then adding 3, then adding 6. However, since addition is commutative, the final result will always be 45. This is a basic function and usage of this class.

Expanded Functionality #

Let’s continue to look at its powerful functionality. Here are a few examples. In the expression we provided earlier, x + y, you can also pass x * y, or write Math.min(x, y), which is equivalent to finding the minimum of x and y. Similarly, you can find Math.max(x, y), which is equivalent to finding the maximum value. Choose according to the requirements of your business. The code is as follows:

LongAccumulator counter = new LongAccumulator((x, y) -> x + y, 0);

LongAccumulator result = new LongAccumulator((x, y) -> x * y, 0);

LongAccumulator min = new LongAccumulator((x, y) -> Math.min(x, y), 0);

LongAccumulator max = new LongAccumulator((x, y) -> Math.max(x, y), 0);

At this point, you may wonder: why not use a for loop here? For example, in our previous example of adding from 0 to 9, why not just write a for loop?

Indeed, a for loop can also meet the requirements, but if you use a for loop, it will be executed serially. It will definitely add in the order of 0 + 1 + 2 + 3 + … + 8 + 9. However, one of LongAccumulator’s major advantages is that it can leverage a thread pool to work for it. Once a thread pool is used, multiple threads can perform parallel calculations, which is much more efficient than previous serial calculations. This is why it was mentioned earlier that the order of addition is not fixed because we cannot guarantee the execution order between different threads. What we can guarantee is that the final result is deterministic.

Suitable Scenarios #

Next, let’s talk about the suitable scenarios for LongAccumulator.

The first condition that needs to be met is that a large amount of calculation is needed, and when parallel computation is required, LongAccumulator can be used.

When the amount of calculation is small and serial calculation can meet the requirements, a for loop can be used; if the amount of calculation is large and the efficiency needs to be improved, we can use a thread pool together with LongAccumulator to achieve parallel computation, which is very efficient.

The second requirement to be met is that the execution order of calculations is not critical, which means it does not require a specific execution order between different calculations. For example, thread 1 may execute after thread 5 or before thread 5, but the order of execution does not affect the final result.

Some very typical calculations that meet this condition are addition or multiplication because they are commutative. Similarly, finding the maximum and minimum values has no requirement for order either because in the end, it will only produce the maximum or minimum value among all the numbers. No matter which value is submitted first or later, it will not affect the final result.