详谈 ConcurrentHashMap 的 size () 方法

74 阅读4分钟
1. 基本功能与设计目标

ConcurrentHashMap.size() 的核心目标是在并发环境下高效地统计元素总数,同时保证弱一致性(不阻塞其他操作,返回值反映某一时刻的近似值)。

  • 非阻塞:不使用全局锁,避免影响其他线程的读写操作。
  • 最终一致性:允许返回值与实际值存在短暂偏差,但最终会收敛到真实值。
2. JDK 1.8 的实现机制

JDK 1.8 的 size() 基于 CounterCell 数组 + CAS 操作 实现,核心思想是将计数分散到多个 Cell 中,减少竞争:

  1. 基础计数变量

    private transient volatile long baseCount;
    private transient volatile CounterCell[] counterCells;
    
    • baseCount:无竞争时直接累加的基础值。
    • CounterCell[]:竞争激烈时,将计数分散到不同的 Cell 中。
  2. 计数流程

    • 无竞争:直接通过 CAS 更新 baseCount

    • 竞争发生

      • 初始化 CounterCell 数组。
      • 通过线程哈希值映射到不同 Cell,使用 CAS 更新对应 Cell 的值。
  3. CounterCell 结构

    @sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }
    
    • @sun.misc.Contended 注解避免伪共享(False Sharing),提升性能。

    伪共享是指多个线程同时读写不同变量,但这些变量恰好位于同一个缓存行(Cache Line)中,导致缓存失效的现象。本质是缓存行层面的资源竞争,虽不直接冲突,但会间接影响性能。

3. size () 方法源码解析
public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;  // 基础值
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;  // 累加所有Cell的值
        }
    }
    return sum;
}

关键点

  • 求和逻辑baseCount + 所有 CounterCell 的值。
  • 弱一致性:遍历过程中其他线程可能修改 Cell 值,导致结果不准确。
4. 并发更新机制

当调用 put() 或 remove() 时,计数更新逻辑如下:

// putVal() 方法中调用
addCount(1L, binCount);

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    
    // 尝试 CAS 更新 baseCount
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        
        // CAS 更新失败,尝试更新 CounterCell
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);  // 初始化 CounterCell 或处理冲突
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
    // ... 后续扩容逻辑
}

流程总结

  1. 优先 CAS 更新 baseCount
  2. 失败则定位到对应 Cell 并 CAS 更新
  3. 若 Cell 为空或仍冲突,调用 fullAddCount() 初始化 Cell 或解决冲突。
5. 与 JDK 1.7 的对比
  • JDK 1.7

    • 基于分段锁(Segment),每个 Segment 维护独立的计数器。
    • size() 需要遍历所有 Segment 并累加计数,可能需要加锁(若其他线程正在修改)。
  • JDK 1.8

    • 移除分段锁,采用 CAS + CounterCell,性能更优。
    • 遍历 Cell 数组无需加锁,弱一致性保证更高吞吐量。
6. 弱一致性的应用场景

size() 的弱一致性适用于:

  • 监控统计:如仪表盘显示集合大小,不要求绝对精确。
  • 批量操作前置条件:如 “元素超过 1000 时触发清理”,允许少量误差。
7.总结

ConcurrentHashMap 的 size() 方法在 JDK 1.8 中通过 CounterCell 数组 + CAS 操作 实现高效并发计数:

  1. 分散计数:将元素总数分散存储在 baseCount 和多个 CounterCell 中,减少竞争。

  2. 无锁更新:线程优先 CAS 更新 baseCount,冲突时按哈希值映射到不同 Cell 进行 CAS 操作。

  3. 最终求和size() 返回 baseCount 与所有 Cell 值的累加和,遍历过程中不阻塞其他操作,因此返回值是弱一致性的。

这种设计相比 JDK 1.7 的分段锁机制,避免了全局锁的开销,在高并发场景下性能更优。但需注意,返回值可能不是实时准确的,适用于对统计精度要求不高的场景。”

8.个人思考
  1. 为什么需要 CounterCell 数组?直接用 AtomicLong 不行吗?

    • 高并发下多个线程竞争同一个 AtomicLong 会导致严重的 CAS 冲突,分散到多个 Cell 可减少竞争(类似 LongAdder 的设计)。
  2. CounterCell 数组如何处理哈希冲突?

    • 通过 ThreadLocalRandom.getProbe() 生成线程哈希值,冲突时通过 fullAddCount() 中的 advanceProbe() 重新生成哈希值,直到成功。
  3. sumCount() 遍历 Cell 数组时,如何保证数据可见性?

    • Cell 数组和元素均被 volatile 修饰,保证线程间的可见性;遍历过程中其他线程的修改可能不被立即感知,体现弱一致性。
  4. 在极端情况下(如所有线程都映射到同一个 Cell),性能会怎样?

    • 极端情况下会退化为类似 AtomicLong 的性能,但概率极低;fullAddCount() 中的冲突处理机制会动态调整线程哈希值,避免长期冲突。