Lazy loaded image
🗒️LongAdder jdk8 源码分析
Words 1632Read Time 5 min
2025-11-13
2025-11-13
type
status
date
slug
summary
tags
category
icon
password
原文
JDK 8 引入的一个高并发计数器类。LongAdder 是 AtomicLong 的高并发优化版本。
核心是将冲突分散不同的槽,减少CAS失败重试次数造成的CPU空转、自旋浪费。
适合读多写少、允许最终一致的统计类操作。
适合场景:
  • 限流、监控 如 Sentinel统计时间窗口内请求量
  • 指标监控、缓存命中次数等

常用方法

  • void increment() 自增
  • void add(long x) 添加指定计数
  • void decrement() 自减
    • increment()、decrement() 都是调用 add方法。
      increment() → add(1L)
      decrement() → add(-1L)
  • long sum() 返回当前总和。这是一个近似值,不保证绝对准确
LongAdder 的构造器没有任何参数,只支持从 0 递增或递减。
 

重要属性

  • Cell[] cells
    • 高并发下分散线程操作,减少竞争
  • transient volatile long base;
    • 用于无竞争的情况下直接更新,或者作用备用更新机制。
  • cellsBusy 锁的标志位。
 

计数操作

计数包括递增、递减、增加指定计数。这底层都是调用 add 方法。

accumulate

LongAdder 内部的cells数组和base两个属性来维护计数器的值。
base 是基础数值,用于低竞争的时更新。在高并发的情况下,更新的失败几率非常大,则去 cells数组中去更新。

longAccumulate

longAccumulate方法的作用
  1. 初始化 cell 数组
  1. 当前线程创建 cell对象加入到 cell 数组中。
    1️⃣
    probe是存储在线程对象 Thread 里的一个整数字段。每个线程都有自己的 probe 值,这个值在每个线程内部是独立的。
    在 LongAdder中probe 值是用来计算数组索引的。
    2️⃣
    当前 Cell 数组已经初始化的情况下,判断当前线程对应的槽位是否为 null。
    数组初始化说明已经有线程开始竞争了。
    对应的槽位为 null,说明当前的槽位还没有线程操作,此时如果是无锁的情况下且拿到锁的情况下,创建 Cell 对象并加入到 cells 数组的槽中。
    更新数组元素时必须拿到锁标志,因为高并发情况下不加锁会导致数组元素覆盖成不正确的情况。
    上述步骤成功以后退出循环,方法结束。
    3️⃣
    根据当前线程的 probe 的计算的数组位置已有 cell元素,直接更新元素值。如果CAS 成功表示计数成功了,直接break 出循环,方法结束。
    4️⃣
    在该步骤之前 有个collide变量 ,该变量是一个碰撞检测标志,用于跟踪线程在尝试更新 Cell 数组的时候有无发生冲突。
    collide = false 表示没有发生碰撞
    collide = true 线程发现目标 Cell 位置被占用且 CAS 更新失败时。
    执行到该处说明并发情况已经非常激烈,尝试扩容 cells 数组来进一步分散热点。
    5️⃣
    初始化数组。首先执行计数操作且第一个拿到锁的线程会在这里进行初始化cells 数组操作。因为执行accumulate方法中的 if 条件是判断 cells ≠ null。LongAdder 在构造器中并没有初始化该数组,所以首先执行的线程肯定会来这里先初始化数组。
    6️⃣
    再次尝试 CAS 更新 base值,可以算是一个兜底或者说降级方案。这里是在 cells数组还没初始化,准备去抢占锁去初始化发现锁已经被别的线程拿到了,cas去更新base值,减少自旋时间的一种尝试。

    数组索引计算

    probe:当前线程的 probe 值,这是一个用于哈希计算的整数值。
    n : cells数组的长度,长度保持在 2 的幂次方。
    index = (n-1) & probe 等价于 (n-1) % probe
    这样能保证索引在[0,n-1]的区间且均匀分布。cells的数组长度也是掩码.
    同 HashMap、ConcurrentHashMap等类计算数组索引一样。
     

    获取计数

    获取计数只能获取快照或者说是近似值。
    LongAdder 读写并不互斥。如果写线程都已计数操作都已完成,那么再去读计数是最终一致的。
    总的计数= base + cells数组中的每个元素的值。
     
    上一篇
    docker部署SpringBoot项目乱码
    下一篇
    MySQL、Redis缓存一致性问题

    Comments
    Loading...