ConcurrentHashMap中的LongAdder思想

一、​​设计背景与核心目标​​

ConcurrentHashMap 在统计元素数量时(如 size() 方法),若直接使用 AtomicLong 会导致以下问题:

  1. ​​CAS 竞争激烈​​:高并发下多个线程频繁竞争同一变量,导致大量 CAS 失败和重试,性能急剧下降
  2. ​​伪共享(False Sharing)​​:多线程操作同一缓存行的不同变量时,缓存一致性协议会引发无效同步,降低性能

为解决这些问题,ConcurrentHashMap 采用类似 LongAdder 的​​分段计数​​机制,将全局计数拆分为多个独立单元,分散线程竞争。


二、​​分段计数实现原理​​

1. ​​核心数据结构​​

  • ​​baseCount​​:基础计数器,用于低并发场景下的原子累加。
  • ​​CounterCell[]​​:分段计数器数组,当检测到 baseCount 竞争激烈时,动态扩展并分配给不同线程使用

2. ​​关键方法逻辑​​

  • ​​addCount(long x, int check)​​:

    1. ​​尝试更新 baseCount​​:通过 CAS 操作直接更新基础值。
    2. ​​竞争失败时切换分段​​:若 CAS 失败,初始化 CounterCell 数组,并根据线程哈希值选择特定分段进行更新。
    3. ​​动态扩容​​:若某分段竞争激烈,自动扩容数组并重新哈希,进一步分散冲突
       
  • ​​size()​​:最终一致性统计,累加 baseCount 和所有 CounterCell 的值。由于不阻塞写操作,可能读到中间状态,但多次调用结果一致

 

3. ​​伪代码示例​

// 伪代码简化版
private void addCount(long x, int check) {
    if (CAS(baseCount, x)) { // 低竞争直接更新
        return;
    }
    // 高竞争时使用分段
    int hash = ThreadLocalRandom.getProbe();
    CounterCell cell = cells[hash & cells.length];
    if (cell == null) {
        // 初始化分段
        cells = new CounterCell[INITIAL_CAPACITY];
    }
    CAS(cell.value, x); // 分段内 CAS 更新
}

  

LongAdder 的核心思想

  1. 分散热点:

    • 传统 AtomicLong:所有线程通过 CAS 竞争更新同一个 value 变量。在高并发下,大量 CAS 失败重试导致性能急剧下降。

    • LongAdder:引入一个 base 变量和一个 Cell[] 数组(初始为空或很小)。

      • 低竞争时: 直接通过 CAS 更新 base 变量(类似 AtomicLong)。

      • 高竞争时: 当线程在更新 base 时遇到 CAS 失败(表示有竞争),它会尝试“分散”:

        • 根据当前线程的某种哈希(如 ThreadLocalRandom)计算一个索引,定位到 Cell[] 数组中的一个 Cell 槽位。

        • 每个 Cell 是一个独立的、填充过的(避免伪共享)、用 volatile 修饰的简单 long 值。

        • 线程优先尝试通过 CAS 更新自己“命中”的那个 Cell 的值。

    • 核心优势: 将原本针对单个变量的全局竞争,分散到了多个 Cell 变量上。只要线程能够相对均匀地映射到不同的 Cell,竞争就大大减少。冲突只在映射到同一个 Cell 的线程间发生,范围大大缩小。

  2. 最终一致求和:

    • 要获取 LongAdder 的当前总和 (sum()),并不是一个简单的读操作。

    • 它需要将 base 的值加上 Cell[] 数组中所有非空 Cell 的值。

    • 这个求和过程在并发进行时,读到的 base 和各个 Cell 的值可能不是严格意义上同一时刻的快照(即不是原子的),因此 sum() 的结果是一个最终一致的近似值。

    • 核心优势: 牺牲了读取时绝对的原子性和实时一致性,换取了极高的写入(更新)性能。这对于像统计计数器这样读少写多且对瞬时绝对精确性要求不高的场景是完美的权衡。

ConcurrentHashMap 中的借鉴(主要在 addCount 方法)

ConcurrentHashMap 使用 sizeCtl 和一些辅助字段管理容量控制,并使用 baseCount 和一个 CounterCell[] 数组(LongAdder 中的 Cell[] 的变体)来统计元素数量。

  1. baseCount (类比 LongAdder.base):

    • 一个普通的 volatile long 变量。

    • 尝试路径: 当需要增加计数(例如在 putVal 成功插入后调用 addCount(1L, binCount)),ConcurrentHashMap 首先尝试用 CAS 直接更新 baseCount (U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x))。如果 CAS 成功,更新就完成了,非常高效。

  2. CounterCell[] (类比 LongAdder.cells):

    • 一个延迟初始化的数组,元素类型是 CounterCell(通常就是一个简单的 volatile long 包装)。

    • 分散路径: 如果步骤 1 中对 baseCount 的 CAS 失败了(表明存在竞争),ConcurrentHashMap 就会进入分散更新模式:

      • 检查 CounterCell[] 是否已初始化。如果没有,尝试初始化一个较小的大小(通常是 2)。

      • 使用线程相关的哈希值(如 ThreadLocalRandom.getProbe())计算一个索引 i

      • 尝试获取对应索引 i 处的 CounterCell

        • 如果该位置为空,尝试新建一个 CounterCell 并放入数组(需要 CAS 保证线程安全)。

        • 如果该位置不为空,尝试用 CAS 更新这个 CounterCell 的值 (U.compareAndSwapLong(c, CELLVALUE, v = c.value, v + x))。

      • 如果在更新选定的 CounterCell 时也遇到 CAS 失败,说明该槽位竞争也激烈,可能会尝试重新计算哈希(ThreadLocalRandom.advanceProbe)映射到另一个槽位,或者尝试扩容 CounterCell[] 数组(如果竞争持续激烈且数组未达上限)。

    • 核心思想体现: 将对单一 baseCount 的竞争分散到多个 CounterCell 上,显著减少线程间的冲突。

  3. 获取元素数量 (size()mappingCount()):

    • 类似于 LongAdder.sum()ConcurrentHashMap 的 size() 或更推荐的 mappingCount() 方法并不是简单地返回 baseCount

    • 它们需要遍历整个 CounterCell[] 数组(如果已初始化),将所有非空 CounterCell 的值累加到 baseCount 上。

    • 核心思想体现: 这个求和过程是非原子的。在并发修改时,返回的值是一个估计值,可能略微偏高或偏低(因为可能漏掉某个 CounterCell 刚更新的值,或者包含某个 CounterCell 刚被加但尚未合并的值)。这就是为了换取高性能更新而接受的最终一致性。

总结:ConcurrentHashMap 中的 LongAdder 思想

  1. 优先尝试 CAS 更新基础值 (baseCount)。

  2. 遇到基础值更新竞争失败时,将更新操作分散到一组 CounterCell 槽位上。

  3. 获取总和 (size()) 需要遍历求和所有分散的值 (baseCount + ΣCounterCell[i].value)。

  4. 读取总和 (size()) 的结果是最终一致性的,不是绝对精确的瞬时值,但写入/更新 (addCount) 的性能在高并发下远优于基于单一原子变量的方案。

这种设计完美契合了 ConcurrentHashMap 的需求:更新计数(putremove)操作极其频繁且要求高性能,而读取计数(size())操作相对较少且可以容忍一定的延迟和不精确性。LongAdder 的思想是解决高并发计数器性能问题的经典模式,并被 ConcurrentHashMap 成功采纳用于其内部计数机制。

posted @ 2025-07-08 20:33  飘来荡去evo  阅读(13)  评论(0)    收藏  举报