23 LongAdder

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);
    }
}
  1. casBase来对base进行操作,如果成功,则不进行后续逻辑;
  2. 如果第一步失败且celss不为null,则从cells中取一个,然后将其+x;
  3. 从cells中取元素的下标是getProbe()返回的;一会儿再看这个方法;
  4. 取的这个元素如果为null或者cas操作失败,则调用longAccumulate方法;
  5. 注意前面无论是对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效率问题,使用数组优化一下,这里涉及到几个问题:

  1. 数组的最大长度,ArrayList中定义的是Integer.MAX_VALUE - 8;也说明了不同的虚拟机可能会预留一些空间,否则会抛出OutOfMemoryError;
  2. 实际测试JDK1.8 HotSpot 可以创建的最大大小是:Integer.MAX_VALUE - 2;
  3. 使用数组代替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在效率上还是快不少的;

  1. 结果是4294967296,果断拿出计算器,7fffffff,转10进制然后乘以2得到:4294967294,差值是2,一个是0,一个是Integer.MIN_VALUE;这意味着,这种计算方式将所有可能的int都出现了一遍后才发生重复!!!实在是厉害了;前面实例化一百万大小的set实在是小看他了;

  2. 重复的第一个数字是:-1640531527,即9e3779b9;继续的话就是下一轮循环;

  3. 将数字换成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);
}
  1. 如果对应的cell为null,则对其进行初始化,成功则返回,初始化时会利用cas操作设置cellsBusy这个volatile的int;cellsBusy登场;
  2. wasUncontended为false时,表示在调用该方法前CAS已经失败过了,那么直接advanceProbe(最后一句,改变获取的cell的下标);
  3. 如果cell不为null,尝试cas操作,将要加的数字加到对应的cell上,成功则返回;
  4. n >= NCPU || cells != as,这个条件成立,则不会对cells进行扩容,这意味着n >= NCPU后,cells不会再扩容,因为散列正常就不会产生冲突了,再多的cell也没有价值了;cells != as,证明别的线程扩容了,那么这一次和下一次不去扩容,collide貌似就是这个作用,试两次,不行再扩容,另一个作用就是 如果 n >= NCPU满足,阻止扩容;
  5. 如果前面的判断都失效,最后一个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;

你可能感兴趣的:(23 LongAdder)