LongAdder and LongAccumulator in Java – 在Java中的LongAdderLongAccumulator

最后修改: 2017年 4月 30日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this article, we’ll be looking at two constructs from the java.util.concurrent package: LongAdder and LongAccumulator.

在这篇文章中,我们将研究来自java.util.concurrent包的两个构造。LongAdderLongAccumulator

Both are created to be very efficient in the multi-threaded environment and both leverage very clever tactics to be lock-free and still remain thread-safe.

两者都是为了在多线程环境中非常有效而创建的,并且都利用了非常巧妙的策略来实现无锁,并且仍然保持线程安全。

2. LongAdder

2.LongAdder

Let’s consider some logic that’s incrementing some values very often, where using an AtomicLong can be a bottleneck. This uses a compare-and-swap operation, which – under heavy contention – can lead to a lot of wasted CPU cycles.

让我们来考虑一些逻辑,这些逻辑会经常递增一些值,其中使用AtomicLong会成为一个瓶颈。这使用了一个比较和交换操作,在严重争用的情况下,会导致大量的CPU周期被浪费。

LongAdder, on the other hand, uses a very clever trick to reduce contention between threads, when these are incrementing it.

另一方面,LongAdder使用了一个非常聪明的技巧来减少线程之间的争夺,当这些线程在递增它的时候。

When we want to increment an instance of the LongAdder, we need to call the increment() method. That implementation keeps an array of counters that can grow on demand.

当我们想要增加一个LongAdder的实例时,我们需要调用increment()方法。该实现保留了一个可以按需增长的计数器数组

And so, when more threads are calling increment(), the array will be longer. Each record in the array can be updated separately – reducing the contention. Due to that fact, the LongAdder is a very efficient way to increment a counter from multiple threads.

于是,当更多的线程在调用increment()时,数组将更长。数组中的每条记录都可以单独更新–减少争论。由于这一事实,LongAdder是一种非常有效的方式,可以从多个线程中递增一个计数器。

Let’s create an instance of the LongAdder class and update it from multiple threads:

让我们创建一个LongAdder类的实例,并从多个线程更新它。

LongAdder counter = new LongAdder();
ExecutorService executorService = Executors.newFixedThreadPool(8);

int numberOfThreads = 4;
int numberOfIncrements = 100;

Runnable incrementAction = () -> IntStream
  .range(0, numberOfIncrements)
  .forEach(i -> counter.increment());

for (int i = 0; i < numberOfThreads; i++) {
    executorService.execute(incrementAction);
}

The result of the counter in the LongAdder is not available until we call the sum() method. That method will iterate over all values of the underneath array, and sum those values returning the proper value. We need to be careful though because the call to the sum() method can be very costly:

在我们调用sum()方法之前,LongAdder中的计数器的结果是不可用的。该方法将遍历下面数组的所有值,并对这些值进行求和,返回适当的值。但我们需要小心,因为对sum()方法的调用可能非常昂贵。

assertEquals(counter.sum(), numberOfIncrements * numberOfThreads);

Sometimes, after we call sum(), we want to clear all state that is associated with the instance of the LongAdder and start counting from the beginning. We can use the sumThenReset() method to achieve that:

有时,在我们调用sum()之后,我们想要清除与LongAdder实例相关的所有状态,并从头开始计算。我们可以使用sumThenReset()方法来实现这一目的。

assertEquals(counter.sumThenReset(), numberOfIncrements * numberOfThreads);
assertEquals(counter.sum(), 0);

Note that the subsequent call to the sum() method returns zero meaning that the state was successfully reset.

请注意,随后对sum()方法的调用返回0,意味着状态被成功重置。

Moreover, Java also provides DoubleAdder to maintain a summation of double values with a similar API to LongAdder.

此外,Java还提供了DoubleAdder来维护double值的相加,其API与LongAdder相似。

3. LongAccumulator

3.LongAccumulator

LongAccumulator is also a very interesting class – which allows us to implement a lock-free algorithm in a number of scenarios. For example, it can be used to accumulate results according to the supplied LongBinaryOperator – this works similarly to the reduce() operation from Stream API.

LongAccumulator也是一个非常有趣的类–它允许我们在许多情况下实现无锁算法。例如,它可以用来根据提供的LongBinaryOperator累积结果–这与Stream API中的reduce()操作类似。

The instance of the LongAccumulator can be created by supplying the LongBinaryOperator and the initial value to its constructor. The important thing to remember that LongAccumulator will work correctly if we supply it with a commutative function where the order of accumulation does not matter.

LongAccumulator的实例可以通过向其构造函数提供LongBinaryOperator和初始值来创建。重要的是要记住,LongAccumulator将正确工作,如果我们为它提供一个换元函数,其中积累的顺序并不重要。

LongAccumulator accumulator = new LongAccumulator(Long::sum, 0L);

We’re creating a LongAccumulator which will add a new value to the value that was already in the accumulator. We are setting the initial value of the LongAccumulator to zero, so in the first call of the accumulate() method, the previousValue will have a zero value.

我们正在创建一个LongAccumulator,它ch将把一个新的值添加到已经在累积器中的值。我们将LongAccumulator的初始值设置为0,所以在第一次调用accumulate()方法时,previousValue将有一个0值。

Let’s invoke the accumulate() method from multiple threads:

让我们从多个线程调用accumulate()方法。

int numberOfThreads = 4;
int numberOfIncrements = 100;

Runnable accumulateAction = () -> IntStream
  .rangeClosed(0, numberOfIncrements)
  .forEach(accumulator::accumulate);

for (int i = 0; i < numberOfThreads; i++) {
    executorService.execute(accumulateAction);
}

Notice how we’re passing a number as an argument to the accumulate() method. That method will invoke our sum() function.

注意我们是如何将一个数字作为参数传递给accumulate()方法的。该方法将调用我们的sum()函数。

The LongAccumulator is using the compare-and-swap implementation – which leads to these interesting semantics.

LongAccumulator正在使用比较和交换的实现–这导致了这些有趣的语义。

Firstly, it executes an action defined as a LongBinaryOperator, and then it checks if the previousValue changed. If it was changed, the action is executed again with the new value. If not, it succeeds in changing the value that is stored in the accumulator.

首先,它执行一个定义为LongBinaryOperator的动作,然后它检查previousValue是否改变。如果它被改变了,就用新的值再次执行这个动作。如果没有,它就成功地改变了存储在累积器中的值。

We can now assert that the sum of all values from all iterations was 20200:

我们现在可以断言,所有迭代的所有数值之和为20200

assertEquals(accumulator.get(), 20200);

Interestingly, Java also provides DoubleAccumulator with the same purpose and API but for double values.

有趣的是,Java也提供了DoubleAccumulator,具有相同的目的和API,但针对double值。

4. Dynamic Striping

4.动态剥落

All adder and accumulator implementations in Java are inheriting from an interesting base-class called Striped64Instead of using just one value to maintain the current state, this class uses an array of states to distribute the contention to different memory locations. 

Java 中的所有加法器和累加器的实现都继承自一个有趣的基类,该基类被称为 Striped64该类不是只使用一个值来维持当前状态,而是使用一个状态数组来将争用分配到不同的内存位置。

Here’s a simple depiction of what Striped64 does:

以下是对Striped64所做工作的简单描述。

Dynamic Striping

Different threads are updating different memory locations. Since we’re using an array (that is, stripes) of states, this idea is called dynamic striping. Interestingly, Striped64 is named after this idea and the fact that it works on 64-bit data types.

不同的线程在更新不同的内存位置。由于我们使用的是一个数组(也就是条纹)的状态,这个想法被称为动态条纹。有趣的是,Striped64是以这个想法和它在64位数据类型上工作的事实命名的。

We expect dynamic striping to improve the overall performance. However, the way the JVM allocates these states may have a counterproductive effect.

我们期望动态条带化能够提高整体性能。然而,JVM分配这些状态的方式可能会产生相反的效果。

To be more specific, the JVM may allocate those states near each other in the heap. This means that a few states can reside in the same CPU cache line. Therefore, updating one memory location may cause a cache miss to its nearby states. This phenomenon, known as false sharing, will hurt the performance.

更具体地说,JVM可能会将这些状态在堆中相互靠近分配。这意味着几个状态可以驻留在同一个CPU缓存行中。因此,更新一个内存位置可能会导致其附近状态的缓存缺失这种现象被称为虚假共享,将损害性能

To prevent false sharing. the Striped64 implementation adds enough padding around each state to make sure that each state resides in its own cache line:

为了防止错误的共享,Striped64实现在每个状态周围添加了足够的填充,以确保每个状态驻留在自己的缓存行中。

False Sharing

The @Contended annotation is responsible for adding this padding. The padding improves performance at the expense of more memory consumption.

@Contended注解负责添加这种填充。填充可以提高性能,但代价是消耗更多的内存。

5. Conclusion

5.结论

In this quick tutorial, we had a look at LongAdder and LongAccumulator and we’ve shown how to use both constructs to implement very efficient and lock-free solutions.

在这个快速教程中,我们看了一下LongAdderLongAccumulator,我们展示了如何使用这两个结构来实现非常高效和无锁的解决方案。

The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven project, so it should be easy to import and run as it is.

所有这些例子和代码片段的实现都可以在GitHub项目中找到–这是一个Maven项目,所以应该很容易导入并按原样运行。