• AtomicLong与LongAdder(下)


    首先 Cell 被 @sun.misc.Contended 修饰。意思是让Java编译器和JRE运行时来决定如何填充。不理解不要紧,不影响理解。

    其实一个 Cell 的本质就是一个 volatile 修饰的 long 值,且这个值能够进行 cas 操作。

    回到我们的 add() 方法。

    这里涉及四个额外的方法 casBase() , getProbe() , a.cas() , longAccumulate();

    我们看名字就知道 casBase() 和 a.cas() 都是对参数的 cas 操作。

    getProbe() 的作用,就是根据当前线程 hash 出一个 int 值。

    longAccumlate() 的作用比较复杂,之后我们会讲解。

    所以这个 add() 操作归纳以后就是:

    1. 如果 cells 数组不为空,对参数进行 casBase 操作,如果 casBase 操作失败。可能是竞争激烈,进入第二步。
    2. 如果 cells 为空,直接进入 longAccumulate();
    3. m = cells 数组长度减一,如果数组长度小于 1,则进入 longAccumulate()
    4. 如果都没有满足以上条件,则对当前线程进行某种 hash 生成一个数组下标,对下标保存的值进行 cas 操作。如果操作失败,则说明竞争依然激烈,则进入 longAccumulate().

    可见,操作的核心思想还是基于 cas。但是 cas 失败后,并不是傻乎乎的自旋,而是逐渐升级。升级的 cas 都不管用了则进入 longAccumulate() 这个方法。

    下面就开始揭开 longAccumulate 的神秘面纱。

     

    longAccumulate 看上去比较复杂。我们慢慢分析。

    回忆一下,什么情况会进入到这个 longAccumulate 方法中,

    • cell[] 数组为空,
    • cell[i] 数据的某个下标元素为空,
    • casBase 失败,
    • a.cas 失败,
    • cell.length - 1 < 0

    在 longAccumulate 中有几个标记位,我们也先理解一下

    • cellsBusy cells 的操作标记位,如果正在修改、新建、操作 cells 数组中的元素会,会将其 cas 为 1,否则为0。
    • wasUncontended 表示 cas 是否失败,如果失败则考虑操作升级。
    • collide 是否冲突,如果冲突,则考虑扩容 cells 的长度。

    整个 for(;;) 死循环,都是以 cas 操作成功而告终。否则则会修改上述描述的几个标记位,重新进入循环。

    所以整个循环包括如下几种情况:

    1. cells 不为空
      1. 如果 cell[i] 某个下标为空,则 new 一个 cell,并初始化值,然后退出
      2. 如果 cas 失败,继续循环
      3. 如果 cell 不为空,且 cell cas 成功,退出
      4. 如果 cell 的数量,大于等于 cpu 数量或者已经扩容了,继续重试。(扩容没意义)
      5. 设置 collide 为 true。
      6. 获取 cellsBusy 成功就对 cell 进行扩容,获取 cellBusy 失败则重新 hash 再重试。
    1. cells 为空且获取到 cellsBusy ,init cells 数组,然后赋值退出。
    2. cellsBusy 获取失败,则进行 baseCas ,操作成功退出,不成功则重试。

    至此 longAccumulate 就分析完了。之所以这个方法那么复杂,我认为有两个原因

    1. 是因为并发环境下要考虑各种操作的原子性,所以对于锁都进行了 double check。
    2. 操作都是逐步升级,以最小的代价实现功能。

    最后说说 LongAddr 的 sum() 方法,这个就很简单了。

     

    就是遍历 cell 数组,累加 value 就行。LongAdder 余下的方法就比较简单,没有什么可以讨论的了。

    LongAdder VS AtomicLong

    看上去 LongAdder 性能全面超越了 AtomicLong。为什么 jdk 1.8 中还是保留了 AtomicLong 的实现呢?

    其实我们可以发现,LongAdder 使用了一个 cell 列表去承接并发的 cas,以提升性能,但是 LongAdder 在统计的时候如果有并发更新,可能导致统计的数据有误差。

    如果用于自增 id 的生成,就不适合使用 LongAdder 了。这个时候使用 AtomicLong 就是一个明智的选择。

    而在 Sentinel 中 LongAdder 承担的只是统计任务,且允许误差。

    总结

    LongAdder 使用了一个比较简单的原理,解决了 AtomicLong 类,在极高竞争下的性能问题。但是 LongAdder 的具体实现却非常精巧和细致,分散竞争,逐步升级竞争的解决方案,相当漂亮,值得我们细细品味。

  • 相关阅读:
    基于SSM+SpringBoot+VUE前后端分离的停车场管理系统
    @RabbitListener和@RabbitHandler的使用
    Linux 系统垃圾日志清理
    Docker Compose
    约定式提交 commit 规范
    Yield Guild Games:社区更新——2022 年第二季度
    小脑萎缩患者平时生活中应该注意哪些?
    【第一阶段:java基础】第5章:数组、排序、查找
    [解题报告] CSDN竞赛第11期
    Linux操作系统入门(适用java软件开发)
  • 原文地址:https://blog.csdn.net/eric_chen1990/article/details/125518092