ConcurrentHashMap实现大小获取的size()函数是怎么实现的

156 阅读2分钟

Java8:

  • 直接返回基础值:若没有竞争,直接返回 baseCount
  • 分散累加:存在竞争时,当多个线程同时更新 baseCount 导致 CAS 失败时,会创建 CounterCell 对象,最后的大小为 baseCount 和所有 CounterCell 总和,得到近似总数。

size()方法的最大值是 Integer 类型的最大值,而 Map 的 size() 有可能超过 Integer.MAX_VALUE,所以 JDK8 建议使用 mappingCount(),而不是size(),因为这个方法的返回值是 long 类型,不会因为 size() 方法是 int 类型限制最大值。


详细

ConcurrentHashMap 的 size() 方法在实现时需要在并发环境下平衡性能与准确性。其具体实现机制在 Java 7Java 8 中存在显著差异,以下是分版本解析:


一、Java 7 的实现(基于分段锁)

Java 7 的 ConcurrentHashMap 采用 Segment 分段锁结构,每个 Segment 维护独立的 HashEntry 数组。size() 的实现逻辑如下:

  1. 两次无锁统计
    首次遍历所有 Segment,累加各 Segment 的 count(volatile 修饰,保证可见性)和 modCount(记录修改次数的变量)。随后进行第二次统计,若两次的 modCount 总和一致,则说明统计期间没有并发修改,直接返回累加的 count 总和。

  2. 加锁重试
    如果两次统计的 modCount 不一致,则对所有 Segment 加锁(阻塞写操作),再次遍历统计所有 count,确保准确性。

设计权衡:通过两次无锁统计降低锁竞争概率,仅在检测到并发修改时才加锁,兼顾性能与准确性。


二、Java 8 的实现(基于 CAS + 分段计数)

Java 8 移除了 Segment,采用 数组 + 链表/红黑树 结构,并通过 baseCountCounterCell 数组实现分布式计数:

  1. 基础计数(baseCount)
    无竞争时,直接通过 CAS 操作更新 baseCount(volatile 修饰的变量)。

  2. 竞争处理(CounterCell 数组)
    当多个线程同时更新 baseCount 导致 CAS 失败时,会创建 CounterCell 对象(类似 LongAdder 的分段计数机制)。每个线程通过哈希分配到不同的 CounterCell,减少竞争。

  3. 最终统计(sumCount())
    统计时累加 baseCount 和所有 CounterCell 的值。由于 CounterCell 使用 @Contended 注解避免伪共享,进一步优化性能。

核心代码逻辑

final long sumCount() {
    CounterCell[] cs = counterCells;
    long sum = baseCount;
    if (cs != null) {
        for (CounterCell c : cs)
            if (c != null)
                sum += c.value;
    }
    return sum;
}

三、设计优化与对比

维度Java 7Java 8
数据结构Segment + HashEntry数组 + 链表/红黑树 + CounterCell
锁机制分段锁CAS + synchronized(桶级别锁)
计数方式两次无锁统计 + 加锁重试分布式计数(baseCount + CounterCell)
性能瓶颈分段锁粒度较粗更细粒度的锁,并发度更高