阅读 LongAdder 源码

自己在复习CAS相关时,突然想到了一个面试题:
项目乐观锁(超高并发下,怎么解决?)
比如:AtomicLong 使用内部变量 value 保存着实际的 long 值,所有的操作都是针对该 value 变量进
行。也就是说,高并发环境下,value 变量其实是一个热点,也就是 N 个线程竞争一个热点。重试
线程越多,意味着 CAS 的失败几率更高,CAS 失败几率就越高,从而进入恶性 CAS 空自旋状态。
LongAdder 的基本思路就是分散热点,将 value 值分散到一个数组中,不同线程会命中到数
组的不同槽(元素)中,各个线程只对自己槽中的那个值进行 CAS 操作。这样热点就被分散了,
冲突的概率就小很多。那么此时就想到了LongAdder,关于LongAdder的其他概念这里不再解释,直接上源码角度(Jdk_1.8.0_191),看看这个是什么套路?

先来看看LongAdder的add()方法:

public void add(long x) {
        //cells 的引用
        Cell[] as;
        //b base   v 期望值
        long b, v;
        //表示 cells 的长度
        int m;
        //a 表示当前线程命中的单元格
        Cell a;
        //条件一: true 表示cells已经初始化好了 当前线程应该将数据写入到对应的 cells 中;直接进入if代码块
        //               false 表示 cells 未被初始化 ,当前所有线程应该将数据写到 base 中;进入下一个判断条件
        //条件二:false  表示当前线程替换数据成功;好的达到目的 直接退出代码块
        //              true 竞争发生 casBase 失败 需要扩容 或者 重试;进入if代码块
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            //什么时候会进来?
            //1. 条件一  成立 cells 已经初始化好了 当前线程应该将数据写入到对应的 cells 中
            //2. 条件二  成立 cells 未被初始化 竞争发生失败 需要扩容 或者 重试

            //true 未发生竞争  false 发生了竞争
            boolean uncontended = true;

            //条件一:(as == null || (m = as.length - 1) < 0  
            //成立:说明cell未被初始化  也就是多线程写base出现竞争了;直接接入if代码块
            //不成立:说明cell已经被初始化了,当前线程应该是 找自己的cell的值;进入下一个判断条件
            //条件二:getProbe()  获取当前线程的hash值     
            //成立:(cells已经开好了) 当前线程对应的下标元素为 null,需要创建;直接接入if代码块
            //不成立:当前线程对应的下标元素不为null,说明下一步需要将 x 追加到 此cell 中; 进入下一个判断条件
            //条件三:a.cas(v = a.value, v + x)  :                 
            //成立:说明cas赋值完成;取反 false 直接 退出代码块,完事儿了;
            //不成立: 说明当前线程对应得 cell 存在竞争,一般uncontended都是false;进入if代码块
            if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[getProbe() & m]) == null ||
                    !(uncontended = a.cas(v = a.value, v + x))) {
                //进来这里的情况?
                //1. 条件一  成立 说明 cell 未被初始化 也就是多线程写 base 出现竞争了 需要重试初始化 cells
                //2. 条件二  成立 说明当前线程对应的下标元素为 null,需要创建;
                //3. 条件三  成立 说明当前线程对应的 cell 存在竞争,需要重试;
                longAccumulate(x, null, uncontended);
            }
        }
    }
主要是 Striped64.longAccumulate() 在使劲:
    //进来这里的情况?
    //1.条件一 成立 说明cell未被初始化 也就是多线程写 base 出现竞争了需要重试初始化cells
    //2.条件二 成立 说明cell已被初始化 当前线程对应的 cell 为 null,需要创建
    //3.条件三 成立 说明cell已被初始化 当前线程对应的 cell 存在竞争,需要扩容
    final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
        //表示线程hash值
        int h;
        //条件成立:说明还未分配 hash 值 需要重新分配hash值
        // 这个if相当于给线程生成一个非0的hash值
        if ((h = getProbe()) == 0) {
            //给当前线程分配 hash 值
            ThreadLocalRandom.current(); // force initialization
            h = getProbe();
            //默认 hash为 0 的线程肯定是写入到了 0 这个 槽位,不把它当作是真正的竞争
            //true 未发生竞争    false 发生了竞争
            wasUncontended = true;
        }

        //表示扩容 意向:false 一定不扩容  true 可能会扩容
        boolean collide = false;
        // 自旋
        for (; ; ) {
            //表示 cells 引用
            Cell[] as;
            //当前线程命中的cell
            Cell a;
            //表示cells 数组长度
            int n;
            //期望值
            long v;

            //CASE1 除非是 还未被初始化 转 CASE2
            //条件一:成立:Cells 已经被初始化了,当前线程需要把数据写入到对应的 cell 中;进入当前if分支
            //             不成立: 转363行代码 进行第二个判断 目的是为了初始化 cells
            if ((as = cells) != null && (n = as.length) > 0) {
                //进来的前提条件:
                //2.条件二 成立 说明 cell 已被初始化 当前线程对应的 cell 为 null,需要创建
                //3.条件三 成立 说明 cell 已被初始化 当前线程对应的 cell 存在竞争,需要重试|扩容

                // 成立的话:表示当前线程对应的 cells 下标 cells 为空 需要创建 new Cell 实例
                if ((a = as[(n - 1) & h]) == null) {
                    // 成立的话 表示当前是无锁状态 锁未占用
                    if (cellsBusy == 0) {
                        //新的cell 元素
                        Cell r = new Cell(x);
                        //是无锁状态 且 能获取到锁  且 尝试加锁(防止线程并发操作)
                        if (cellsBusy == 0 && casCellsBusy()) {
                            //是否创建 成功 标记
                            boolean created = false;
                            // Recheck under lock 在有锁的情况下再检测一遍之前的判断
                            try {              
                                //表示当前 cells 引用
                                Cell[] rs;
                                //表示长度
                                int m, j;
                                //考虑别的线程可能执行了扩容,这里重新赋值重新判断
                                if ((rs = cells) != null && (m = rs.length) > 0 &&
                                        //当 当前线程发生了退出cpu 又回到cpu 时
                                        //为了防止其他线程在此期间初始化过该位置  然后当前线程再次初始化此位置  
                                        //导致   数据覆盖(如果被覆盖了就不需要覆盖了 需要再次寻址)
                                        rs[j = (m - 1) & h] == null) {
                                    //对没有使用的Cell单元进行累积操作(第一次赋值相当于是累积上一个操作数,
                                    //求和时再和base执行一次运算就得到实际的结果)
                                    rs[j] = r;
                                    //创建完毕
                                    created = true;
                                }
                            } finally {
                                //释放锁
                                cellsBusy = 0;
                            }
                            //如果原本为null的Cell单元是由自己进行第一次累积操作,
                            //那么任务已经完成了,所以可以退出循环
                            if (created) {
                                break;
                            }
                            //不是自己进行第一次累积操作,重头再来
                            continue;           // Slot is now non-empty
                        }
                    }
                    //强制设置为不扩容
                    //执行这一句是因为cells被加锁了,不能往下继续执行第一次的赋值操作(第一次累积),
                    //所以还不能考虑扩容
                    collide = false;
                }

                // 此时 线程对应的 cell 元素不为空 只是上一个add()函数调用cas 赋值时 失败 就进来了这个分支
                // wasUncontended :只有cells 初始化后  并且 当前线程 是 竞争修改失败 才会 false,
                //也就是只会执行一次
                //CAS already known to fail add()函数执行CAS更新a.value(进行一次累积)
                //的尝试已经失败了,说明已经发生了线程竞争
                else if (!wasUncontended) {     
                    //情况失败标识,然后跳出此 if 去执行最后代码块的 重新算一遍线程的hash值 然后重新 循环
                    wasUncontended = true;
                }     // Continue after rehash

                //线程对应位置不为空 && 上一个if分支也执行过了 线程也重新执行rehash了 
                //就来到了这里 再次尝试执行cas追加操作
                //一般都是 fn == null 直接更新 v+x
                //当前线程 rehash 过 新命中的 cell 不为空
                //如果写入成功 就退出自旋  ;
                // 否则 表示 rehash  之后 命中的新cell 也存在竞争 重试1次
                else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) {
                    //执行成功 完事了 要是没有成功就再继续rehash线程 继续循环
                    break;
                }

                //线程对应位置不为空 && 上一个if分支也执行过了 线程也重新执行rehash了 &&
                //相应的新位置 cell 执行cas追加操作失败
                //条件一:如果数组长度大于cpu数量 成立就表示扩容意向为 false;如果没有大于 就走下一个判断条件
                //条件二:如果 cells != as 表示当前有其他线程已经 扩容过了,当前线程 rehash 过重试即可
                else if (n >= NCPU || cells != as) {
                    //表示不扩容了
                    collide = false; //长度n是递增的,执行到了这个分支,说明n >= NCPU会永远为true,
                    //下面两个else if就永远不会被执行了,也就永远不会再进行扩容
                }    // At max size or stale

                //collide 取反成立后 设置扩容意向  并不一定 真的 发生扩容
                else if (!collide) {
                    //把扩容意向设置为true,只有这里才会给collide赋值为true,也只有执行了这一句,
                    //才可能执行后面一个else if进行扩容
                    collide = true;
                }

                //线程对应位置不为空 && 上一个if分支也执行过了 线程也重新执行rehash了 && 
                //相应的新位置 cell 执行cas追加操作失败 && 执行扩容

                //真正扩容的代码
                //条件一:cellsBusy == 0 表示无锁状态,当前线程可以去竞争这把锁
                //                    成立:走下一个判断条件
                //                 不成立: 直接 走到代码最后 再次给当前线程重新 rehash 机会
                //条件二:casCellsBusy()   并且 通过原子方式 获取锁
                //                    成立:走进代码内部
                //                 不成立:不会走到这一步
                else if (cellsBusy == 0 && casCellsBusy()) {
                    try {
                        //检查下是否被别的线程扩容了(CAS更新锁标识,处理不了ABA问题,这里再检查一遍)
                        if (cells == as) {
                            //执行2倍扩容
                            Cell[] rs = new Cell[n << 1];
                            for (int i = 0; i < n; ++i) {
                                rs[i] = as[i];
                            }
                            //扩容完毕 内存可见性也设置了
                            cells = rs;
                        }
                    } finally {
                        //释放锁
                        cellsBusy = 0;
                    }
                    //回归扩容意向
                    collide = false;
                    // Retry with expanded table 扩容后重头再来
                    continue;                 
                }

                //重新给线程生成一个hash值,降低hash冲突,减少映射到同一个Cell导致CAS竞争的情况
                h = advanceProbe(h);
            }

            // Cells 没有被初始化,并且也没有被加锁(被别的线程正在初始化)
            //那么就尝试对它进行加锁,加锁成功进入这个else if
            // CASE2 前置条件:cells 还未被初始化,需要被初始化
            // 条件一:cellsBusy == 0  成立:说明 当前未加锁;
            // 条件二: cells == as 再次对比 因为 害怕之前 多线程之前已经执行初始化了;
            // 条件二: casCellsBusy()  表示获取锁成功  ;失败 表示其他锁持有这个锁;
            else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
                boolean init = false;
                // Initialize table
                try {
                    // 又来对比 防止其他线程已经初始化了 当前线程又来进行初始化 造成数据丢失
                    if (cells == as) {//CAS避免不了ABA问题,这里再检测一次,
                    //如果还是null,或者空数组,那么就执行初始化
                        Cell[] rs = new Cell[2];//初始化时只创建两个单元
                        // h为线程的hash值, 对其中一个单元进行累积操作,另一个不管,继续为 null
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        //初始化结束
                        init = true;
                    }
                } finally {
                    // 清空自旋标识,释放锁
                    cellsBusy = 0;
                }
                // 如果某个原本为null的Cell单元是由自己进行第一次累积操作,
                //那么任务已经完成了,所以可以退出循环
                if (init) {
                    //初始化完毕了 + 线程对应的 cell 值也 赋值好了 那就完事了  直接退出代码块
                    break;
                }
            }

            //CASE3 :前提 上面 CASE1 把cells初始化过的处理了; 
            //CASE2 把cells初始化处理了; CASE3就是处理 初始化cells失败的兜底方案;
            // 1. casCellsBusy() false -> 当前 CellsBusy 加锁状态 or casCellsBusy() 方法执行失败:
            //表示其他线程正在初始化 cells  所以当前线程需要把数据累计到 base 中
            // 2. cells == as false ->Cells 被其他线程初始化后   所以当前线程需要把数据累计到 base 中
            else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) {
                //成功加到base后 就完事了 ; 没有加到的话 继续for循环
                break;
            }  // Fall back on using base
        }//for(;;) over
    }//longAccumulate() over

总结:在并发情境下,为了保证共享变量的线程安全,首先提出了 synchronized 同步锁来保证并发线程安全,万事都有优点和缺点,优点是编程操作简单,缺点是最后的重量级锁线程之间的用户态和内核态的 切换耗费性能;然后就出现了CAS机制,不让线程去阻塞,而是采用自旋的方式来尝试线程安全,达到数据一致性;再后来的CAS机制还是有缺点,可能会导致大量的线程自旋消耗CPU性能,之后出现了资源均分概念,可以分摊热点,如此就能减少CPU消耗,以空间换时间,那么LongAdder对于未来还会有什么缺点,以及有什么新的理念来进行创新呢?

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值