1、AtomicInteger
这里主要看incrementAndGet方法
可以看出,AtomicInteger的累加方法使用了unsafe中的getAndAddInt方法(这里的U是jdk.internal.misc.Unsafe类)
AtomicInteger之所以使用unsafe进行数据累加而不适用volatile,就是因为volatile不能保证数据的原子性,而unsafe可以。我们看到,unsafe中的累加方法是通过CAS实现的,多线程并发时,同时只会有一个修改成功,其它的线程会不停的在这里自旋,知道修改成功为止。(这里补充记录一点CAS在汇编语言层面的一点知识,CAS在汇编语言层面会转化为一条指令:compxchg去修改内存中的值,并发执行时,这条指令会被上锁,lock compxchg)
性能:高并发执行下性能是偏差的,因为同时只有一个线程修改成功,其他线程就只能不停的自旋。
2、AtomicLong
atomicLong中的incrementAndGet 方法和atomicInteger中差不多,只是操作的数据类型不一样。
这里就不再赘述了,也是通过CAS方法自旋修改数据的。这里主要是通过AtomicLong引出longAdder
3、LongAdder
上边已经分析过,AtomicLong可以保证原子性,但是高并发下,性能太低,假设有20个线程同时做累加,那么同时就只能有一个线程累加成功,另外19个就得在那里自旋。为了解决这个性能问题,就产生了LongAdder
那么LongAdder是怎么做累加的呢,先看下我手画的简易图,然后分析源码
longAdder中引入了Cell,简单说一下高并发情况下调用longAdder中的累加方法increment的大致流程:
看源码
这个源码看着稍微有点迷糊,可以简单说一下:
红色标记1:判断cells数组是否被初始化(已经初始化的话往下走)
红色标记2:cells数组没有被初始化,就将数据通过CAS方法写入base(写入不成功才往下走)
蓝色标记1:cells数组是否被初始化(没有被初始化的话往下走)
蓝色标记2:cells数组已经被初始化了,判断当前线程是否命中到对应的cell(没有命中到的话往下走)
蓝色标记3:cells数组已经被初始化了,当前线程也命中到对应cell了,开始通过CAS方法对cell中的value值做累加(这里可能有多线程竞争,竞争失败的,继续往下走)
下边是实际场景的执行顺序:
1:首先如果cells数组是空的,也就是没有被初始化,就会通过CAS方法尝试将数据写入base中(这时候如果有其他线程竞争,也是有可能写入base失败的,写入失败后,后续方法中会再一次尝试往base里边写,或者写入cell,都是有可能的,后边源码分析可以看到)
2:如果cells被初始化了,再进来写数据的线程就不会再往base里边写数据了,会通过当前线程获取一个int值,再与cells数组长度-1做位运算,简单说就是找到当前线程往哪个cell里写数据,如果找不到对应的cell,后续方法中会去创建对应的cell,然后将数据写入
3:如果第二步中通过当前线程找到了对应的cell,就会通过CAS方法将数据写入cell,因为这里也是有竞争关系的,多个线程可能命中同一个cell,都要对这个cell中的value值做累加,这时候就需要竞争,竞争成功的写入cell,竞争失败的,后续方法会再一次写入cell(当然这个过程是有点复杂的,可能会修改当前线程对应的int值,重新调整命中的cell进行写入,或者对cells进行扩容后尝试写入)
大概的数据写入base和cells的流程就是这样子,写入完后可以调用sum方法,将这两块数据一次性累加得出最后结果,所以高并发下性能比AtomicLong要高许多,但是我们也可以看出,longAdder它不是十分可靠的,因为LongAdder的add方法和sum方法是分开的,你调用完add方法再调用sum方法,有可能有些线程还没执行完add方法,造成你sum出来的结果不准确。
到这里大致的流程就可以结束了,有兴趣的可以继续往下看。
上边分析到的LongAdder的大致执行流程中都提到了一个后续方法,这个后续发放有点复杂,
就是 longAccumulate 方法,下边上源码
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
//这里我们可以看成是通过当前线程获取的一个hash值
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current(); // force initialization
h = getProbe();
wasUncontended = true;
}
boolean collide = false; // True if last slot nonempty
//这里搞了一个死循环,直到成功写入为止
done: for (;;) {
//cs:cells数组;c:当前线程命中的cell;n:cells数组长度;v:内存中的期望值
Cell[] cs; Cell c; int n; long v;
//1:数组已经被初始化,而且cells长度大于0的情况下才会进入
if ((cs = cells) != null && (n = cs.length) > 0) {
//1.1:当前线程没有命中到对应cell,进入此方法
if ((c = cs[(n - 1) & h]) == null) {
if (cellsBusy == 0) { // Try to attach new Cell
Cell r = new Cell(x); // Optimistically create
if (cellsBusy == 0 && casCellsBusy()) {
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;
break done;
}
} finally {
cellsBusy = 0;
}
continue; // Slot is now non-empty
}
}
collide = false;
}
//1.2:不存在竞争关系进入
//在上边蓝色标记3中,竞争写入cell失败的线程会传入false
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
//1.3:竞争写入cell失败的线程在第一次循环到1.2时会修改wasUncontended 的值
//再一次循环时就会跳过1.2,来到1.3这里再次尝试写入cell
else if (c.cas(v = c.value,
(fn == null) ? v + x : fn.applyAsLong(v, x)))
break;
//1.4:再一次写入cell失败后,会考虑要不要对cells扩容
//若数组长度大于等于CPU数量,或者在这期间有其他线程已经修改了cells,则不
//进行扩容
else if (n >= NCPU || cells != cs)
collide = false; // At max size or stale
//1.5:修改扩容标记,若之前都是不扩容的,能走到这里就给设置成扩容,以便执行1.6
else if (!collide)
collide = true;
//1.6:写入cell又一次失败了,决心对cells进行扩容,扩容前先获取cells锁,获取成
//功就对cells进行扩容
else if (cellsBusy == 0 && casCellsBusy()) {
try {
if (cells == cs) // Expand table unless stale
//可以看出,这里对cells是2倍扩容
cells = Arrays.copyOf(cs, n << 1);
} finally {
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
//这里有个关键的,每次循环进入此处执行完都会修改当前线程对应的int值,以便将数据
//写入新的cell中
h = advanceProbe(h);
}
//2:如果数组没有被初始化,这里会尝试获取cells的锁,拿到的话进入
else if (cellsBusy == 0 && cells == cs && casCellsBusy()) {
try { // Initialize table
if (cells == cs) {
//cells的初始化长度为2,此后也会以2的倍数扩容
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
break done;
}
} finally {
cellsBusy = 0;
}
}
//3:cells数组没有被初始化,尝试获取cells数组锁也获取失败,
//就会走这里,尝试将数据写入base,当然,有竞争的情况下也是有可能写入失败的
// Fall back on using base
else if (casBase(v = base,
(fn == null) ? v + x : fn.applyAsLong(v, x)))
break done;
}
}
这里我对源码做了注释,但是看起来不免还有点头大,下边截图分块梳理可能会清晰点
1:cells数组已经被初始化了
2:cells数组没有被初始化,这期间也没有其他线程对它初始化,就尝试获取cells的锁,获取成功就对cells进行初始化
3:cells没有被初始化(或者这期间被其他线程初始化了),尝试获取cells锁也获取失败了,就尝试将数据写入base
最复杂的是第一个分支
1.1:当前线程没有命中到对应cell,进入此方法
1.2:不存在竞争关系进入(在上边蓝色标记3中,竞争写入cell失败的线程会传入false)
1.3:竞争写入cell失败的线程在第一次循环到1.2时会修改wasUncontended 的值,再一次循环时就会跳过1.2,来到1.3这里再次尝试写入cell
1.4:再一次写入cell失败后,会考虑要不要对cells扩容,若数组长度大于等于CPU数量,或者在这期间有其他线程已经修改了cells,则不进行扩容
1.5:修改扩容标记,若之前都是不扩容的,能走到这里就给设置成扩容,以便执行1.6
1.6:写入cell又一次失败了,决心对cells进行扩容,扩容前先获取cells锁,获取成功就对cells进行扩容
第二个分支
这个相对简单,就是对cells数组扩容,这里可以看到cells数组初始长度是2,后边扩容也是以2倍扩容;另外这里有个点需要解释下,就是蓝线标注的地方,为什么进行两次比较?
原因:因为高并发场景下,线程1比较完cells数组是没有被扩容过的,然后尝试去拿锁,这时候CPU时间可能用完,线程1被挂起,线程2拿到锁对cells进行扩容了,这时候线程1又获得CPU时间继续执行,会将扩容好的cells重新扩容,覆盖掉线程2写入的数据,所以这里在扩容cells之前需要再次判断。
第三个分支
这个就比较简单了,就是通过CAS的方法尝试将数据写入base中
到这里LongAdder中的increment方法就全部梳理完了,下边说一点扩展知识。
Q:为什么设计Cell?
A:为了提高高并发情况下,数据的写入效率
Q:LongAdder中为什么大量使用volatile,而不使用synchronized?
A:LongAdder中应用了cell,高并发情况下可以将数据写入多个cell中,只要保证数据的可见性就可以了,通过synchronized加锁反而影响LongAdder中发放的执行效率。
Q:为什么Cell要使用contended注解?
A:这里要先搞清楚contended注解是干嘛用,这个注解的作用就是解决伪共享。什么意思呢。
我们都知道,数据是有CPU缓存的,真正的数据在主存(也就是内存)中,如果线程1修改了cell的value值,线程1会将数据写入到主存,线程2如果读取CPU缓存时和线程1读取的是同一个缓存行cache line(线程读取CPU缓存时是以行读取的),这时候这个cache line已经被线程1修改了,所以线程2必须取主存中读取数据,哪怕线程2和线程1读取的cell不是同一个(两个cell可能在同一个cache line中,如果不加contended注解的话),这就会使性能变差。
contended是怎么解决这个问题的呢,就是它是怎么保证每个被标记的类单独占一个cache line呢?空间换时间,它会给标记的类添加很多的空值,因为一个cache line是64个字节,给标记的类添加空置,使对象占用的字节大于64就可以保证被标记的类独占一个cache line,就是这么简单粗暴。对contended注解有兴趣的同学可以看一下 JVM系列之:Contend注解和false-sharing 这篇文章,算讲的很透彻了。