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

[超级链接:Java并发学习系列-绪论]
[系列概述: Java并发22:Atomic系列-原子类型整体概述与类别划分]


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

1.原子类型累加器

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

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

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

  • 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.
 *
 * 

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. * *

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 freqs}, * initializing if not already present, you can use {@code * freqs.computeIfAbsent(k -> new LongAdder()).increment();} * *

This class extends {@link Number}, but does not 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 freqs进行增量计算,可以使用freqs.computeIfAbsent(k -> new LongAdder()).increment();

这个类继承自Number类,但是并未定义equals()/hashCode()/compareTo()等方法。
因为实例对象预计是变动的,所以并不适于作为集合类型的Key。

3.内部实现浅谈

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

热点分离简述:

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

热点分离优缺点:

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

4.基本方法学习

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

  1. LongAdder():累加器只有一个无参的构造器,会构造一个sum=0的实例对象。
  2. increment():自增。
  3. decrement():自减。
  4. add(delat):增量计算。
  5. sum():计算sum的和。
  6. reset():重置sum为0。
  7. sumThenReset():计算sum的和并且重置sum为0。
  8. intValue():获取sum的int形式(向下转型)。
  9. floatValue():获取sum的float形式(向上转型)。
  10. 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循环依次启动。
  • 每个线程分别对LongAdderAtomicLong对象进行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的性能显著提升。

你可能感兴趣的:(Java并发,Java并发学习实例)