Java并发27:Atomic系列-原子类型累加器XxxxAdder和XxxxAccumulator的学习笔记

本章主要对原子累加器进行学习。

1.原子类型累加器

原子类型累加器是JDK1.8引进的并发新技术,它可以看做AtomicLong和AtomicDouble的部分加强类型。

为什么叫部分呢?是因为原子类型累加器适用于数据统计,并不适用于其他粒度的应用。

原子类型累加器有如下四种:

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

本文的内容以LongAdder作为学习对象。

2.源码解读

先看一下LongAdder的注释源码:

/**
 * One or more variables that together maintain an initially zero
 * {@code long} sum.  When updates (method {@link #add}) are contended
 * across threads, the set of variables may grow dynamically to reduce
 * contention. Method {@link #sum} (or, equivalently, {@link
 * #longValue}) returns the current total combined across the
 * variables maintaining the sum.
 *
 * <p>This class is usually preferable to {@link AtomicLong} when
 * multiple threads update a common sum that is used for purposes such
 * as collecting statistics, not for fine-grained synchronization
 * control.  Under low update contention, the two classes have similar
 * characteristics. But under high contention, expected throughput of
 * this class is significantly higher, at the expense of higher space
 * consumption.
 *
 * <p>LongAdders can be used with a {@link
 * java.util.concurrent.ConcurrentHashMap} to maintain a scalable
 * frequency map (a form of histogram or multiset). For example, to
 * add a count to a {@code ConcurrentHashMap<String,LongAdder> freqs},
 * initializing if not already present, you can use {@code
 * freqs.computeIfAbsent(k -> new LongAdder()).increment();}
 *
 * <p>This class extends {@link Number}, but does <em>not</em> define
 * methods such as {@code equals}, {@code hashCode} and {@code
 * compareTo} because instances are expected to be mutated, and so are
 * not useful as collection keys.
 *
 * @since 1.8
 * @author Doug Lea
 */
public class LongAdder extends Striped64 implements Serializable {//...}

 

上面的代码翻译如下:

  • 一个或者多个变量共同维护一个初始为0的sum值。
  • 当多线程之间调用更新方法add()产生竞争时,数据集会动态地进行扩充,以此来减少争用。
  • sum()方法会返回当前维持sum值的数据集的总和。
  • 当多个线程共同维护一个共享变量进行数据统计时,使用LongAdder的性能要优于AtomicLong。
  • 当然,LongAdder并不适用于更加细粒度的同步控制。
  • 在低并发环境下,这两个类的性能表现是类似的。
  • 但是在高并发环境下,LongAdder会有显著的性能提高,但是也会消耗较高的空间作为牺牲。
  • LongAdder可以被用于一个ConcurrentHashMap来维持这个可伸缩的数据集。
  • 例如,为ConcurrentHashMap<String,LongAdder> freqs进行增量计算,可以使用freqs.computeIfAbsent(k -> new LongAdder()).increment();
  • 这个类继承自Number类,但是并未定义equals()/hashCode()/compareTo()等方法。
  • 因为实例对象预计是变动的,所以并不适于作为集合类型的Key。

3.内部实现浅谈

原子类型累加器其实是应用了热点分离思想,这一点可以类比一下ConcurrentHashMap的设计思想。

热点分离简述:

  • 将竞争的数据进行分解成多个单元,在每个单元中分别进行数据处理。
  • 各单元处理完成之后,通过Hash算法进行计算求和,从而得到最终的结果。

热点分离优缺点:

  • 热点分离的设计减小了锁的粒度提高了高并发环境下的吞吐量
  • 热点分离的设计需要划分额外的空间进行单元数据的存储,增大了空间消耗。

4.基本方法学习

下面以LongAdder为例,对原子类型累加器的基本方法进行学习:

  • LongAdder():累加器只有一个无参的构造器,会构造一个sum=0的实例对象。
  • increment():自增。
  • decrement():自减。
  • add(delat):增量计算。
  • sum():计算sum的和。
  • reset():重置sum为0。
  • sumThenReset():计算sum的和并且重置sum为0。
  • intValue():获取sum的int形式(向下转型)。
  • floatValue():获取sum的float形式(向上转型)。
  • doubleValue():获取sum的double形式(向上转型)。

实例代码:

/*
 LongAdder所使用的思想就是热点分离,这一点可以类比一下ConcurrentHashMap的设计思想。
 就是将value值分离成一个数组,当多线程访问时,通过hash算法映射到其中的一个数组进行计数而最终的结果,就是这些数组的求和累加。这样一来,就减小了锁的粒度.

 1.LongAdder和LongAccumulator是AtomicLong的扩展
 2.DoubleAdder和DoubleAccumulator是AtomicDouble的扩展
 3.在低并发环境下性能相似;在高并发环境下---吞吐量增加,但是空间消耗增大
 4.多用于收集统计数据,而非细粒度计算
  */

//构造器
LongAdder adder = new LongAdder();
System.out.println("默认构造器:" + adder);

//自增
adder.increment();
System.out.println("increment():自增----" + adder);

//自减
adder.decrement();
System.out.println("decrement():自减----" + adder);

//增量计算
System.out.println("------------add(long):增量计算:");
int sum = 0;
long add;
for (int i = 0; i < 5; i++) {
    add = RandomUtils.nextLong(100, 300);
    sum += add;
    adder.add(add);
    System.out.println("增加---" + add + "-->" + sum);
}

//最终的值
System.out.println("sum():最终值---" + adder.sum());

//重置sum值
adder.reset();
System.out.println("reset():重置值---" + adder);

//获得最终的值并重置
System.out.println("------------再次增量计算:");
sum = 0;
for (int i = 0; i < 5; i++) {
    add = RandomUtils.nextLong(100, 300);
    sum += add;
    adder.add(add);
    System.out.println("增加---" + add + "-->" + sum);
}
System.out.println("sumThenReset():获取最终值并重置---" + adder.sumThenReset());
System.out.println("重置值---" + adder);

//多种形式返回值
System.out.println("------------多种数据类型返回值:");
adder.add(RandomUtils.nextLong(100, 200));
System.out.println("int类型:" + adder.intValue());
System.out.println("double类型:" + adder.doubleValue());
System.out.println("float类型:" + adder.floatValue());

运行结果:

默认构造器:0
increment():自增----1
decrement():自减----0
------------add(long):增量计算:
增加---280-->280
增加---250-->530
增加---252-->782
增加---164-->946
增加---221-->1167
sum():最终值---1167
reset():重置值---0
------------再次增量计算:
增加---269-->269
增加---127-->396
增加---252-->648
增加---107-->755
增加---161-->916
sumThenReset():获取最终值并重置---916
重置值---0
------------多种数据类型返回值:
int类型:164
double类型:164.0
float类型:164.0

4.高并发性能测试

验证目标:

高并发环境下LongAdder的性能要优于AtomicLong。

验证场景:

  • 定义num个线程,这些线程通过for循环依次启动。
  • 每个线程分别对LongAdder和AtomicLong对象进行perNum次自增操作。
  • 分别测试当num、perNum的值为100、1000、10000、100000时的时间消耗。

实例代码:

//测试AtomicLong
final long start = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
    new Thread(() -> {
        for (int j = 0; j < perNum; j++) {
            atomicLong.incrementAndGet();
//          longAdder.increment();
        }
        System.out.println(atomicLong + "----" + (System.currentTimeMillis() - start));
//      System.out.println(longAdder + "----" + (System.currentTimeMillis() - start));
    }).start();
}

测试结果:

num perNum  AtomicLong  LongAdder  比例
100  100  60ms  60ms   约1:1
1000  1000  182ms  200ms  约1:1
10000  1000  1830ms  1700ms  约1.1:1
10000  10000  3161ms  2410ms  约1.3:1
100000  10000  23333ms  11981ms  约2:1
100000  100000 201850ms  38843ms   约5.2:1

 


总结:
虽然这个测试的结果并不能精准的反应LongAdderAtomicLong的性能差别。
但是从运行结果上,可以大体的体会到,在高并发环境下LongAdder的性能显著提升。

 

posted @ 2021-08-26 15:11  姚春辉  阅读(352)  评论(0编辑  收藏  举报