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