【Java源码计划】LongAdder<rt.jar_ja
LongAdder
源码解读
源码解读部分按照我得理解翻译和解读注解并添加相关的部分代码解读
保持一个或者多个变量,初始值设置为零用于求和。当出现多个线程竞争进行一个数的更新时,这个变量集合可以动态的扩展。最后当需要求和的时候或者说需要这个Long型的值时,可以通过把当前这些变量求和,合并后得出最终的和。
这个类在一些多线程环境下表现要比AtomicLong这个类好,比如多个线程同时更新一个求和的变量,比如统计集合的数量,但是不能用作细粒度的同步控制变量,换句话说这个是可能有误差的。在低并发竞争的情况下LongAdder和AtomicLong的性能表现没什么差别,但是当高并发竞争的时候,这个类将具备更好的吞吐性能,但是相应的也会耗费相当的空间。
LongAdder类可以和ConcurrentHashMap一起用一个可扩展的增长序列。例如一个统计一个柱状图或者统计变量的集合之类的功能,如果没有初始化这个key对应value可以通过jdk1.8在map接口中提供的函数 ConcurrentHashMap.computeIfAbsent(k -> new LongAdder().increment());来实现对是否存在的判断和加一的操作。
LongAdder继承了Number抽象类,但是并没有定义一些方法例如,equals,hashCode,compareTo,因为实例的期望用途是进行一些比较频繁的变化,所以自然也就不需要用于作为集合的key。
public LongAdder()
默认构造方法会初始化一个新的adder并将和初始化为0.
public LongAdder() {
//没什么特殊的
}
public void add(long x)
将给定的值增加进去。
public void add(long x) {
//此处涉及的Cell类与Striped64有关
Cell[] as;
long b, v;
int m;
Cell a;
//casBase方法来源于Striped64,放到对应的地方细讲
//casBase会尝试着进行修改base的操作,如果成功,那就执行结束
//如果失败意味着发生多线程竞争,会采取措施
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
//通过哈希值来获取当前线程对应的cells数组中的位置getProbe()
//获取该位置上的cell,如果该cell不为null,
//那么就试图将本次计数累计到该cell上,a.cas()
//如果不成功,那么就需要Striped64类的longAccumulate方法来进行计数累计
//涉及的Cell类的方法和Striped64类中的方法会放到对应的地方
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
以下简要介绍一下Striped64
Striped64是在java8中添加用来支持累加器的并发组件,它可以在并发环境下使用来做某种计数,Striped64的设计思路是在竞争激烈的时候尽量分散竞争,在实现上,Striped64维护了一个base Count和一个Cell数组,计数线程会首先试图更新base变量,如果成功则退出计数,否则会认为当前竞争是很激烈的,那么就会通过Cell数组来分散计数,Striped64根据线程来计算哈希,然后将不同的线程分散到不同的Cell数组的index上,然后这个线程的计数内容就会保存在该Cell的位置上面,基于这种设计,最后的总计数需要结合base以及散落在Cell数组中的计数内容。
类似的设计思想——java7的ConcurrentHashMap实现,也就是所谓的分段锁算法,ConcurrentHashMap会将记录根据key的hashCode来分散到不同的segment上,线程想要操作某个记录只需要锁住这个记录对应着的segment就可以了,而其他segment并不会被锁住,其他线程任然可以去操作其他的segment,这样就显著提高了并发度。
但是注意,虽然这个思想很棒,但是java8中的ConcurrentHashMap实现已经抛弃了java7中分段锁的设计,而采用更为轻量级的CAS来协调并发。
LongAdder就是基于Striped64实现的计数器
Striped64的详细内容我们会放到Striped64的源码中说
public void increment()
方法等价于调用add(1L)方法
public void increment() {
//等价于调用了add方法
add(1L);
}
public void decrement()
方法等价于调用add(-1L)方法
public void decrement() {
//等价于调用了add方法
add(-1L);
}
public long sum()
返回当前值,注意返回的数值并不是符合原子性的值快照,在没有并发更新的情况下调用将返回准确的结果,但是在统计计算总和的时候发生的并发更新可能并不会合并进去。
public long sum() {
Cell[] as = cells; Cell a;
//统计base的值和各个cell的值得和
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
public void reset()
重置所有用于统计总和的变量为0,可以利用这个方法创建一个新的加法器,但是只有在没有并发更新的情况下才有效。因为这个方法本质上是不稳定的,所以只有当知道没有线程同时更新时才可以使用。
public void reset() {
Cell[] as = cells; Cell a;
//先把base置为0
base = 0L;
//然后将每个cell中的值置为0
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
a.value = 0L;
}
}
}
public long sumThenReset()
方法等价于先求和后进行reset操作,也就是先调用sum然后调用reset。例如,这个方法可以用于求多线程情况下在一个瞬间的稳定计算值。在方法执行期间,如果仍然有线程在进行并发更新,那么返回的值可能是不一致的。(参考源码)
public long sumThenReset() {
Cell[] as = cells; Cell a;
long sum = base;
base = 0L;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null) {
//求和的同事将cell置为零
sum += a.value;
a.value = 0L;
}
}
}
return sum;
}
public String toString()
返回和的值转换为string
public String toString() {
return Long.toString(sum());
}
public long longValue()
等价于调用sum方法。
public long longValue() {
return sum();
}
public int intValue()
将结果执行强制类型转换为int类型。
public int intValue() {
return (int)sum();
}
public float floatValue()
将结果执行强制类型转换为float类型。
public float floatValue() {
return (float)sum();
}
public double doubleValue()
将结果执行强制类型转换为double类型。
public double doubleValue() {
return (double)sum();
}
源码中接下来是一个静态内部类
private static class SerializationProxy
是一个序列化的代理,用于避免引用序列化中的非公共Striped64