ConcurrentHashMap.size() 为什么“不靠谱”?答案比你想的复杂

前几天在优化一段并发代码时,我用到了 ConcurrentHashMap 来存储共享数据。在某个逻辑里,我需要获取这个 map 的元素数量,下意识地就调用了 size() 方法。结果却发现,这个方法的表现和我熟悉的 HashMap.size() 有点不一样------有时候返回的数量并不准确,甚至会随着并发操作轻微波动。后来查了一下资料才知道,ConcurrentHashMapsize() 方法在设计上本就和普通的 HashMap 不一样,核心原因是出于并发性能的优化和线程安全之间的权衡。那么,它到底是怎么设计的?我们在实际使用中该如何正确处理这种情况呢?


ConcurrentHashMap 的 size() 方法在设计上与普通的 HashMap 不同,其核心原因是 并发环境下的性能优化线程安全的权衡

JDK 1.7 的实现:分段锁 + 多次尝试

在 JDK 1.7 中,ConcurrentHashMap 使用 分段锁(Segment) 结构,每个 Segment 是一个独立的小型哈希表,包含自己的锁和计数器(count)。size() 方法的实现分为两步:

  1. 无锁尝试 :遍历所有 Segment,累加每个段的 count 值。如果连续两次遍历的结果一致,则认为当前没有并发修改,返回结果。

  2. 加锁重试 :如果无锁尝试失败(即结果不一致),则对所有 Segment 加锁,重新计算总大小。

为什么不能直接返回一个全局变量?

  1. 分段锁设计 :每个 Segmentcount 是独立维护的,没有全局的 size 变量。直接维护全局变量会导致 锁竞争,降低并发性能。

  2. 弱一致性 :并发修改时,size() 返回的是一个近似值,而非实时精确值。这种设计牺牲了绝对准确性,但大幅提升了性能。


JDK 1.8 的实现:CAS + CounterCell 分段统计

在 JDK 1.8 中,ConcurrentHashMap 放弃了分段锁,改用 CAS(Compare and Swap)CounterCell 数组 来优化 size() 的计算。核心机制如下:

  • baseCount:基础计数器,记录未发生竞争时的元素数量。
  • CounterCell[] :当多个线程并发更新 baseCount 失败时,会通过 CAS 更新 CounterCell 数组中的某个元素,最终通过 baseCount + sum(CounterCell[]) 得到总大小。

计算流程

  1. 调用 size() 时,通过 sumCount() 方法累加 baseCount 和所有 CounterCell 的值。
  2. 最终结果是baseCount + 所有CounterCell中值的总和
  3. put()remove() 等操作会通过 CAS 更新 baseCountCounterCell,确保线程安全。
java 复制代码
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;
        }
    }
    return sum;
}

为什么不能直接返回一个全局变量?

  • 高并发场景下的性能瓶颈 :如果直接使用一个全局的 volatile int size,每次修改都需要通过 CAS 更新该变量,会导致大量线程竞争,降低吞吐量。而 CounterCell 数组通过 分散竞争(每个线程更新不同的槽位)减少了冲突。
  • 伪共享(False Sharing)优化CounterCell 使用 @sun.misc.Contended 注解,避免多个线程更新相邻内存地址导致的缓存行伪共享问题,进一步提升性能。

为什么 size() 的结果可能不准确?

ConcurrentHashMapsize() 方法设计为 弱一致性,原因如下:

1、性能优先ConcurrentHashMap 的设计目标是 快速返回一个合理近似值 ,而非严格精确值。在高并发场景下,绝对准确的 size() 需要冻结整个表(如加锁),直接使用一个全局变量会导致极高的竞争,成为性能瓶颈这会严重拖慢性能。每次put/remove都需要同步更新这个变量,严重影响并发性能

2、全局锁:计算过程中其他线程可以继续修改哈希表,可能导致结果与实际值存在偏差。

3、LongAdder设计思想: JDK8借鉴了LongAdder的分段计数思想,通过分散热点到多个CounterCell,减少竞争,只有在需要获取大小时才进行汇总计算

4、空间换时间:使用额外的CounterCell数组空间,换取更高的并发更新性能


如何获取更准确的大小?

如果对结果的准确性要求极高,可以通过以下方式替代 size()

1、遍历整个哈希表 :使用迭代器(如 entrySet().size())强制遍历所有键值对,但会阻塞并发修改,性能代价较大。

2、使用 mappingCount() :JDK 1.8 提供 mappingCount() 方法,返回 long 类型的值(避免 size()int 溢出问题),但仍是弱一致性的。


特性 JDK 1.7 JDK 1.8
数据结构 分段锁(Segment) 数组 + 链表/红黑树 + CounterCell
size() 实现 多次无锁尝试 + 分段加锁 CAS + CounterCell 分散更新
性能 中等(分段锁竞争) 高(CAS 和 CounterCell 减少竞争)
准确性 弱一致性 弱一致性
推荐方法 使用 mappingCount()(避免溢出)

ConcurrentHashMapsize() 方法通过分段统计和 CAS 优化,在 性能与准确性之间取得平衡。直接返回全局变量会导致并发瓶颈,而当前的设计通过分散竞争和弱一致性,既保证了高吞吐量,又满足了大多数场景的需求。

相关推荐
轻语呢喃24 分钟前
JavaScript :字符串模板——优雅编程的基石
前端·javascript·后端
MikeWe29 分钟前
Paddle张量操作全解析:从基础创建到高级应用
后端
future141233 分钟前
C#每日学习日记
java·学习·c#
岫珩38 分钟前
Ubuntu系统关闭防火墙的正确方式
后端
一个混子程序员38 分钟前
SpringBoot自定义Schedule注解
java
心之语歌42 分钟前
Java高效压缩技巧:ZipOutputStream详解
java·后端
booooooty1 小时前
基于Spring AI Alibaba的多智能体RAG应用
java·人工智能·spring·多智能体·rag·spring ai·ai alibaba
猴哥源码1 小时前
基于Java+SpringBoot的健身房管理系统
java·spring boot
极光雨雨1 小时前
Spring Bean 控制销毁顺序的方法总结
java·spring
猴哥源码1 小时前
基于Java+SpringBoot的三国之家网站
java·spring boot