[超级链接:Java并发学习系列-绪论]
[系列概述: Java并发22:Atomic系列-原子类型整体概述与类别划分]
本章主要对原子累加器进行学习。
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的性能显著提升。