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

相关推荐
彩票管理中心秘书长7 分钟前
MySQL数据库新建流程和字符集详细介绍
后端
geovindu9 分钟前
go: Proxy Pattern
开发语言·后端·设计模式·golang·代理模式
彩票管理中心秘书长11 分钟前
MySQL 用户与权限管理 (DCL) 操作命令大全
后端
langsiming14 分钟前
【无标题】
java·开发语言·数据库
彩票管理中心秘书长16 分钟前
MySQL 索引、事务与约束操作命令大全
后端
Rust语言中文社区17 分钟前
【Rust日报】2026-04-24 Vizia 0.4 发布——纯 Rust 声明式响应式 GUI 框架
开发语言·后端·rust
weisian15120 分钟前
Java并发编程--45-分布式一致性协议入门:Raft、Paxos与ZAB的核心思想
java·分布式·raft·paxos·zab
木井巳22 分钟前
【递归算法】解数独
java·算法·leetcode·决策树·深度优先·剪枝
t***54427 分钟前
如何在 Dev-C++ 中切换编译器
java·开发语言·c++
Lisonseekpan29 分钟前
Git:如何将一个分支的特定提交合并到另一个分支?
java·大数据·git·后端·elasticsearch