ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【HUC】原子类 LongAdder:从 add() 到 longAccumulate() 源码解析

【HUC】原子类 LongAdder:从 add() 到 longAccumulate() 源码解析

LongAdder的设计哲学是用空间换时间,通过分散热点来应对高并发。add()方法作为入口,其逻辑清晰地体现了这一思想:尽可能在无竞争或低竞争路径上快速返回,将复杂的竞争处理“委托”给longAccumulate()。

📥add(long x):快速路径与竞争委托

LongAdder本身没有定义add,它继承自Striped64。add方法的代码不长,但每一行都对应着一种并发场景。

publicvoidadd(longx){Cell[]as;longb,v;intm;Cella;// 条件一:cells 已初始化 (as != null)// 条件二:!casBase(b = base, b + x) 意味着对 base 的 CAS 更新失败if((as=cells)!=null||!casBase(b=base,b+x)){booleanuncontended=true;// 初始假定无竞争// 进入此分支意味着:要么 cells 已存在,要么 base 的 CAS 失败了// 条件一:cells 为 null(base CAS 失败,但 cells 尚未初始化)// 条件二:cells 长度为 0(理论上不应发生)// 条件三:当前线程 hash 命中的 Cell 为 null(需要新建 Cell)// 条件四:!a.cas(v = a.value, v + x) 对命中的 Cell 进行 CAS 更新失败if(as==null||(n=as.length)<1||(a=as[(n-1)&(m=getProbe())])==null||!(uncontended=a.cas(v=a.value,v+x)))longAccumulate(x,null,uncontended);}}

执行流程解读:

  1. 低竞争快路径:如果cells尚未初始化(as == null),方法会首先尝试对全局的base进行 CAS 累加(casBase)。在并发度极低时,这一步就能成功,add方法直接返回,没有任何额外开销。

  2. base CAS 失败 → 触发复杂逻辑:如果cells已经初始化,或者base的 CAS 操作失败(说明有竞争),就会进入if块,准备在cells数组上操作。

  3. Cell 层面的快速路径:在进入longAccumulate之前,add会做最后一次“挣扎”。它通过getProbe()获取当前线程的探测哈希值m,定位到cells数组中对应的槽位as[(n-1) & m]。如果该槽位已经存在一个Cell对象,它会尝试直接对这个Cell的value进行 CAS 累加。这次 CAS 的结果会被记录在uncontended变量中。如果这次 CAS 成功,方法同样直接返回,无需进入更重量级的longAccumulate。

  4. 委托给longAccumulate:只有当出现以下任一情况时,才会调用longAccumulate:

    • cells数组尚未初始化(但baseCAS 已失败)。
    • 线程命中的槽位为null,需要创建新的Cell。
    • 对已存在的Cell进行 CAS 更新失败(uncontended = false),表明该Cell也出现了竞争。

关键点:add方法通过casBase和Cell.cas两次尝试,尽可能避免进入longAccumulate。传入的wasUncontended参数(即uncontended变量)告知了longAccumulate:“在你接手之前,我已经在 Cell 上尝试过 CAS 并且失败了”,这对于longAccumulate的后续决策至关重要。


🧠longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended):竞争处理核心

这个方法由Striped64提供,是处理初始化、创建Cell、扩容以及解决竞争的总控逻辑。在LongAdder的调用中,fn始终为null(代表简单的加法),但该方法也服务于支持自定义函数的LongAccumulator。

finalvoidlongAccumulate(longx,LongBinaryOperatorfn,booleanwasUncontended){inth;// 1. 确保当前线程拥有一个非零的 probe 哈希值if((h=getProbe())==0){ThreadLocalRandom.current();// 强制初始化线程的 probeh=getProbe();wasUncontended=true;// 新分配 probe,视为无竞争}booleancollide=false;// 扩容意向标志for(;;){// 2. 无限自旋,直至成功Cell[]as;Cella;intn;longv;// CASE 1: cells 数组已初始化if((as=cells)!=null&&(n=as.length)>0){// ...}// CASE 2: cells 未初始化,尝试初始化elseif(cellsBusy==0&&cells==as&&casCellsBusy()){// ...}// CASE 3: 回退,直接累加到 baseelseif(casBase(v=base,((fn==null)?v+x:fn.applyAsLong(v,x))))break;}}

longAccumulate的主体是一个for (;;)自旋循环,它的内部被划分为三个主要的CASE,分别对应cells的不同状态。

前置操作:线程 Probe 的初始化

getProbe()返回当前线程的探测哈希值(probe)。如果为0,意味着该线程尚未被分配probe。此时会调用ThreadLocalRandom.current()强制初始化,并重新获取probe。同时,wasUncontended被强制设为true。原因是,一个新分配probe的线程,其哈希值默认会映射到cells[0],这不被视为一次真正的竞争失败,因此重置竞争标志,给它一次“干净”的重试机会。

CASE 1:cells已初始化 —— 创建Cell、重试或扩容

这是最复杂的路径,当cells数组存在且长度大于0时进入。

if((as=cells)!=null&&(n=as.length)>0){// CASE 1.1: 命中的槽位为 null,需要创建新 Cellif((a=as[(n-1)&h])==null){if(cellsBusy==0){// 无锁状态Cellr=newCell(x);// 乐观地预先创建if(cellsBusy==0&&casCellsBusy()){// 获取锁booleancreated=false;try{Cell[]rs;intm,j;// 双重检查:防止在获取锁期间,其他线程已在该位置创建了 Cellif((rs=cells)!=null&&(m=rs.length)>0&&rs[j=(m-1)&h]==null){rs[j]=r;created=true;}}finally{cellsBusy=0;// 释放锁}if(created)break;// 创建成功,退出自旋continue;// 槽位已被其他线程占用,重试}}collide=false;// 命中空槽位,不需要扩容}// CASE 1.2: 之前已知 CAS 失败,重置标志后重试elseif(!wasUncontended)wasUncontended=true;// 继续循环,使用重新哈希后的 probe 重试// CASE 1.3: 对命中的 Cell 进行 CAS 更新elseif(a.cas(v=a.value,((fn==null)?v+x:fn.applyAsLong(v,x))))break;// 更新成功,退出自旋// CASE 1.4: 已达到扩容上限或 cells 已被其他线程修改,放弃本次扩容意图elseif(n>=NCPU||cells!=as)collide=false;// CASE 1.5: 标记扩容意向(但尚未真正扩容)elseif(!collide)collide=true;// CASE 1.6: 真正执行扩容elseif(cellsBusy==0&&casCellsBusy()){try{if(cells==as){// 再次检查,防止重复扩容Cell[]rs=newCell[n<<1];// 容量翻倍for(inti=0;i<n;++i)rs[i]=as[i];// 迁移已有 Cellcells=rs;}}finally{cellsBusy=0;}collide=false;continue;// 扩容后重试}h=advanceProbe(h);// ★ 关键一步:重新计算当前线程的 probe}

CASE 1 的逻辑链条:

  1. CASE 1.1(空槽位):当前线程的probe映射到一个null槽位。此时会在cellsBusy自旋锁的保护下,通过双重检查(rs[j] == null)安全地创建一个新的Cell并放入。cellsBusy是一个通过 CAS 维护的自旋锁,用于保护cells数组的结构性修改(初始化、扩容、创建 Cell)。

  2. CASE 1.2(重置竞争标志):如果add方法传入的wasUncontended为false(表示在add中 Cell CAS 已失败),这里会将其重置为true。这相当于给该线程一次“申诉”机会:之前失败是因为probe对应的 Cell 有竞争,现在我会重新计算probe(在最后一行advanceProbe(h)),换一个 Cell 再试。

  3. CASE 1.3(Cell CAS 成功):对命中的Cell的value进行 CAS 累加。这是最常见的成功路径,成功后直接break退出自旋。

  4. CASE 1.4 - 1.6(扩容决策):

    • 如果n >= NCPU(数组长度已达 CPU 核数上限)或cells引用已发生变化,则放弃扩容(collide = false)。
    • 如果collide为false,将其设为true。这是一个扩容意向标记,表示“我刚刚在这个 Cell 上又失败了,可能真的需要扩容了”。
    • 如果collide已经是true(意味着上次失败后重试仍然失败),并且成功获取了cellsBusy锁,则真正执行扩容:将数组长度翻倍(n << 1),并迁移所有已有的Cell引用。
  5. h = advanceProbe(h):无论本轮CASE 1的哪个分支被执行(除了成功break或continue到扩容外),最后都会执行这行代码。它使用一个伪随机函数重新计算当前线程的probe。这是实现“分散热点”的核心机制——当在一个Cell上竞争失败后,线程会“换一个地方”再试,而不是死磕同一个槽位。

CASE 2:cells未初始化 —— 安全地完成初始化

当cells为null或长度为0,且cellsBusy锁可用时,进入此分支。

elseif(cellsBusy==0&&cells==as&&casCellsBusy()){booleaninit=false;try{// 双重检查,防止在获取锁期间其他线程已完成初始化if(cells==as){Cell[]rs=newCell[2];// 初始容量为 2rs[h&1]=newCell(x);// 将当前线程的增量放入对应的 Cellcells=rs;init=true;}}finally{cellsBusy=0;}if(init)break;// 初始化成功,退出自旋}

这是cells数组的懒初始化路径。只有在baseCAS 失败后,add才会委托到这里。初始化过程同样受cellsBusy锁保护,并包含cells == as的双重检查,以确保并发环境下初始化操作的原子性。初始数组长度为2,当前线程的增量x会被直接放入rs[h & 1]对应的Cell中。

CASE 3:回退 —— 直接更新base

这是最后的保底路径。

elseif(casBase(v=base,((fn==null)?v+x:fn.applyAsLong(v,x))))break;

当CASE 1(cells存在但处理失败)和CASE 2(无法获取锁来初始化cells)都走不通时,意味着当前有另一个线程正在持有cellsBusy锁(可能在初始化或扩容),或者cells已被其他线程初始化。此时,当前线程会选择回退到最基础的base上进行 CAS 累加。如果这个 CAS 成功,就退出自旋;如果失败,则继续循环重试。


💎 核心机制与设计思想总结

从add到longAccumulate的整个流程,贯穿了以下几个关键设计:

  • 分层竞争处理:性能敏感路径被极致优化。baseCAS →CellCAS →longAccumulate,每一层都试图以最低成本解决问题,将重量级操作限制在真正需要的时候。
  • cellsBusy自旋锁:一个轻量级的 CAS 锁,仅用于保护cells数组的结构性修改(初始化、扩容、创建Cell)。对Cell内部value的更新并不需要此锁,而是通过Cell自身的 CAS 完成。
  • advanceProbe重哈希:这是“分散”策略的引擎。在竞争失败或创建新Cell后,线程的probe会变化,从而在后续重试中可能命中cells数组中的不同槽位,动态地分散访问压力。
  • 有界扩容 (n >= NCPU):cells数组的长度不会无限增长,其上限是 CPU 核数。这避免了过度扩容带来的内存和缓存开销,因为在核数已满的情况下,更多的槽位也无法带来真正的并行度提升。
  • Cell的缓存行填充:Cell是AtomicLong的一个“填充变体”(Padded variant)。它的value字段前后有额外的填充字段,目的是防止伪共享(False Sharing)。这确保了对不同Cell的更新不会因为落在同一个 CPU 缓存行上而相互干扰,是高性能的关键底层优化。

sum()的结果:最终,LongAdder.sum()返回的是base的值与cells数组中所有非null的Cell的value之和。需要注意,sum()方法在并发更新时不保证原子性,它返回的是一个“弱一致”的快照值。

LongAdder通过这套精密的协作机制,在高并发统计求和的场景下,实现了远超AtomicLong的吞吐量,其代价是在空间(Cell数组、填充)和读取时的数据一致性(sum()非原子)上做出了权衡。

相关方法和变量定义

流程图

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进