LongAdder源码阅读
各种value方法:
public long longValue() {
return sum();
}
public int intValue() {
return (int)sum();
}
public float floatValue() {
return (float)sum();
}
public double doubleValue() {
return (double)sum();
}
- 第一眼看重这几个方法是因为其强制类型转换让我产生了疑问,sum方法返回的是一个long类型,这里将其直接转换成int\float\double
-
难道这样转换是正确的吗?参考如下代码:
// 0010 0101 1001 0010 1101 1010 1111 0111 1101 long l = 10086100861L; int i = (int)l; System.out.println(i); long i2 = 0B01011001001011011010111101111101; System.out.println(i2); float f = (float)l; System.out.println(f); double d = (double)l; System.out.println(d); //输出: 1496166269 1496166269 1.0086101E10 1.0086100861E10
long转换成int典型的是做了截断操作,只取了低32位;而转换成float则数字差异比较大,10086100861 变成了float后是10086101000;显然这样的转换数字是存在差异的,只有double转换后数值没有变化;
increment 和decrement方法 都是对add 的调用
public void increment() {
add(1L);
}
/**
* Equivalent to {@code add(-1)}.
*/
public void decrement() {
add(-1L);
}
add方法
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
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);
}
}
- casBase来对base进行操作,如果成功,则不进行后续逻辑;
- 如果第一步失败且celss不为null,则从cells中取一个,然后将其+x;
- 从cells中取元素的下标是getProbe()返回的;一会儿再看这个方法;
- 取的这个元素如果为null或者cas操作失败,则调用longAccumulate方法;
- 注意前面无论是对base的cas操作还是对cells中某个元素的cas操作,均没有自旋,成功当然好,但失败不会重试,最后由longAccumulate方法来确保完成任务;
getProbe方法决定Cells下标:
//这是跟线程相关的,不同的现场会有不同的PROBE;
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
//静态代码块中:
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
//再看Thread中有定义,却没有初始化;如果所有的线程的该变量都是0的话,那么显然会显著增加冲突,那也就失去了Cells的作用了;
//答案在longAccumulate方法中;
int threadLocalRandomProbe
longAccumulate方法:
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
这一部分是对getProbe()方法处理,如果其返回0,证明没有初始化,调用ThreadLocalRandom.current();进行初始化,然后再次获取,并将wasUncontended标志位设置为true,表明cas操作没有失败,需要继续尝试CAS(这里因为重新改变了getProbe()的返回值,所以需要重新CAS操作尝试在某个cell对象上增加值)
看看ThreadLocalRandom.current()做了什么:
public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}
private static final AtomicInteger probeGenerator =
new AtomicInteger();
private static final int PROBE_INCREMENT = 0x9e3779b9;
private static final AtomicLong seeder = new AtomicLong(initialSeed());
private static final long SEEDER_INCREMENT = 0xbb67ae8584caa73bL;
我们这里不关注seeder,仅关注probe,localInit方法会对当前线程的threadLocalRandomProbe变量进行赋值,可以看到probe被赋予了 probeGenerator.addAndGet(PROBE_INCREMENT)的值,不会为0,如果为0,被修正为1;并且每个线程的probe不会相同;
为什么PROBE_INCREMENT 要选择0x9e3779b9这个数字呢,这让我想起了ThreadLocal这个类,其中也有类似的设计:
private static final int HASH_INCREMENT = 0x61c88647;
但其是0x61c88647,这两个值的目的都是一样,尽可能散列,但却值不一样,我猜测是不同的人、不一样的时期、不同的知识结构认知不一样以及不同的场景,技术抉择不同(cells一般长度不会太大,ThreadLocal则不一样);
聊聊 0x9e3779b9这个数字
0x9e3779b9 是黄金分割数0.618((√5-1)/2近似为0.618) * 232次方,也就是232个无符号int整形的黄金分割点;
黄金分割在建筑和艺术上使用较多,认为能够引起人们的美感;难道在程序上也能完美散列?所以写了如下代码:
public static void main(String[] args) {
int number = 0x9e3779b9;
HashSet set = new HashSet<>(1000000);
int temp = number;
long count = 0;
while (!set.contains(temp)) {
set.add(temp);
temp += number;
++count;
}
System.out.println(count);
}
洗完澡回来居然还没有跑完,果断联想到HashSet效率问题,使用数组优化一下,这里涉及到几个问题:
- 数组的最大长度,ArrayList中定义的是Integer.MAX_VALUE - 8;也说明了不同的虚拟机可能会预留一些空间,否则会抛出OutOfMemoryError;
- 实际测试JDK1.8 HotSpot 可以创建的最大大小是:Integer.MAX_VALUE - 2;
- 使用数组代替set时,Integer.MIN_VALUE需要特殊处理:
public class HashNumberSelectTest {
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
public static void main(String[] args) {
int number = 0x9e3779b9;
boolean[] negativeSmall = new boolean[MAX_ARRAY_SIZE];
boolean[] positiveSmall = new boolean[MAX_ARRAY_SIZE];
boolean[] negativeLarge = new boolean[Integer.MAX_VALUE + 1- MAX_ARRAY_SIZE+1];
boolean[] positiveLarge = new boolean[Integer.MAX_VALUE-MAX_ARRAY_SIZE+1];
int temp = number;
long count = 0;
boolean[] buffer = null;
int index= 0;
while (!Thread.interrupted()) {
if(temp >= 0) {
index = temp;
if(temp >= MAX_ARRAY_SIZE) {
buffer = positiveLarge;
index -= MAX_ARRAY_SIZE;
} else{
buffer = positiveSmall;
}
} else {
if(temp == Integer.MIN_VALUE) {
index = MAX_ARRAY_SIZE + temp;
index = -index;
buffer = negativeLarge;
} else {
index = -temp;
if(index >= MAX_ARRAY_SIZE) {
buffer = negativeLarge;
index-= MAX_ARRAY_SIZE;
} else {
buffer = negativeSmall;
}
}
}
if(buffer[index]) {
break;
} else {
buffer[index] = true;
}
temp += number;
++count;
}
System.out.println(count);
}
}
这次依然是洗澡回来,跑完了,所以主观感受上,数组比HashSet在效率上还是快不少的;
结果是4294967296,果断拿出计算器,7fffffff,转10进制然后乘以2得到:4294967294,差值是2,一个是0,一个是Integer.MIN_VALUE;这意味着,这种计算方式将所有可能的int都出现了一遍后才发生重复!!!实在是厉害了;前面实例化一百万大小的set实在是小看他了;
重复的第一个数字是:-1640531527,即9e3779b9;继续的话就是下一轮循环;
将数字换成0x61c88647,测试结果依然是4294967296,他们在int范围内都能够很好的散列;重复的第一个数字是:0x61c88647,意味着继续的话 不过是下一个循环,情况还是一样:所有的数字都会生成一遍最后才会重复;
当然这只显示了这种计算方式在int可承受的范围所出现重复的情况,实际当作下标使用时时,往往容量没有这么大,所以还需要根据实际数组大小,再统计下标重复率才更加贴近实际使用场景;
看看longAccumulate方法的其他部分
先从大概结构上理解一下longAccumulate方法的逻辑:
for (;;) {
if ((as = cells) != null && (n = as.length) > 0){
//如果cells已经被初始化过了;
}
else if (cellsBusy == 0 && cells == as && casCellsBusy()){
//如果cells忙;
}
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x)))){
//(fn == null) 在LongAdder对其的调用中是恒成立的;
//如果对base进行cas操作,v变成v+x,成功则break;
break;
}
}
总结起来就是如果cells被初始化了,则在cells的某个元素上添加(猜测),cells没初始化且忙(稍后看怎么判断忙不忙),则做另一些处理,如果cells没有被初始化,而且其还在忙,那么尝试在base上进行cas操作,成功则算完成任务;
如果cells被初始化了的处理逻辑:
if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == as) { // Expand table unless stale
Cell[] rs = new Cell[n << 1];
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = advanceProbe(h);
}
- 如果对应的cell为null,则对其进行初始化,成功则返回,初始化时会利用cas操作设置cellsBusy这个volatile的int;cellsBusy登场;
- wasUncontended为false时,表示在调用该方法前CAS已经失败过了,那么直接advanceProbe(最后一句,改变获取的cell的下标);
- 如果cell不为null,尝试cas操作,将要加的数字加到对应的cell上,成功则返回;
- n >= NCPU || cells != as,这个条件成立,则不会对cells进行扩容,这意味着n >= NCPU后,cells不会再扩容,因为散列正常就不会产生冲突了,再多的cell也没有价值了;cells != as,证明别的线程扩容了,那么这一次和下一次不去扩容,collide貌似就是这个作用,试两次,不行再扩容,另一个作用就是 如果 n >= NCPU满足,阻止扩容;
- 如果前面的判断都失效,最后一个else if则试尝试对cells进行扩容,直接增大一倍并将原来的cell拷贝过来;
最后如果都不成功会调用advanceProbe方法,该方法将改变当前线程的probe变量,使用了xorshift随机算法,这个算法下次复习的时候来搞定它,意味着下一个循环,获取的cell就改变了;
TODO:xorshift随机算法
如果cells没有被初始化的处理逻辑:
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
逻辑相对比较简单,cells初始化为2个cell的数组,对其中一个cell初始化为要加的值即可;
如果cells没有被初始化,且别的线程在做初始化动作或者初始化完成了(但我们的程序判断是否初始化完成时,别的线程还没有完成初始化),那么尝试将值加到base上:
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
至此,add方法基本就已经看清楚了,cas加到base上,失败,则加对应的cell,再失败,换cell再加,再失败,cells扩容再加;
加在base上的机会只有两个,刚调用add尝试加base,第二个时机则是cells没有初始化,但线程尝试初始化的时候有别的线程在初始化,此时会尝试加在base上,否则只会加在cell上;
sum方法:
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
- sum方法比较简单,看其逻辑没有使用锁,直接将base和cell的和求出来,那么求和过程中,cell、base都是可能发生改变的,所以sum的值并不准确;
- 注释也说了,如果没有并发修改,那么值是准确的,如果有并发修改,那么值就是不正确的,甚至是某个时间点的快照都做不到;这意味着sum返回的数字可能根本就是错误的,而不仅仅是过期的数字这么简单;
问题:
cells虽然是volatile的数组,但是对cells的某个元素的获取是直接使用cells[index]来获取的,但是印象中,ConcurrentHashMap中对于数组的元素的获取是使用了unsafe的getObjectVolatile来获取的,cells[index]能否获得最新的元素引用呢?
从我所了解的理论知识以及ConcurrentHashMap的行为来看,volatile修饰的数组引用是无法保证其元素也是volatile的,但想了很久也没有想到很好的方法来验证这个结论;这里cells获取元素没有使用类似ConcurrentHashMap的获取元素的方法的原因,可能是因为其不会有太高的并发冲突所以没有使用getObjectVolatile方法,其也是有额外开销的,毕竟cells的理想情况是,最多有NCPU个线程是并行的,此时一个线程对应一个cell;