AtomicInteger、AtomicLong、LongAdder的性能问题及源码分析

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 这篇文章,算讲的很透彻了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值