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 优化,在 性能与准确性之间取得平衡。直接返回全局变量会导致并发瓶颈,而当前的设计通过分散竞争和弱一致性,既保证了高吞吐量,又满足了大多数场景的需求。

相关推荐
超级码.里奥.农3 分钟前
零基础 “入坑” Java--- 七、数组(二)
java·开发语言
hqxstudying12 分钟前
Java创建型模式---单例模式
java·数据结构·设计模式·代码规范
挺菜的20 分钟前
【算法刷题记录(简单题)002】字符串字符匹配(java代码实现)
java·开发语言·算法
A__tao20 分钟前
一键将 SQL 转为 Java 实体类,全面支持 MySQL / PostgreSQL / Oracle!
java·sql·mysql
一只叫煤球的猫31 分钟前
真实事故复盘:Redis分布式锁居然失效了?公司十年老程序员踩的坑
java·redis·后端
猴哥源码34 分钟前
基于Java+SpringBoot的农事管理系统
java·spring boot
面朝大海,春不暖,花不开1 小时前
Java网络编程:TCP/UDP套接字通信详解
java·网络·tcp/ip
yanlele1 小时前
前端面试第 75 期 - 2025.07.06 更新前端面试问题总结(12道题)
前端·javascript·面试
慕y2741 小时前
Java学习第十五部分——MyBatis
java·学习·mybatis
大鸡腿同学1 小时前
身弱武修法:玄之又玄,奇妙之门
后端