concurrenthashmap 使用 countercell 数组分散计数竞争而非单一 basecount,其为懒加载 volatile 数组,长度为 2 的幂,上限为 cpu 核心数;size() 返回近似值因无锁快照累加。

ConcurrentHashMap 不用单一变量记录总数,是因为高并发下多个线程同时更新一个 baseCount 会频繁 CAS 失败,造成大量自旋空转、CPU 浪费。CounterCell 的核心作用,就是把“争抢一个数”变成“各自往不同格子里填数”,从而分散压力。
CounterCell 是什么结构
它是一个懒加载的 volatile 数组,每个元素是 CounterCell 类型,内部仅含一个 volatile long value。这个数组不参与数据存储,只服务于 size() 和 mappingCount() 这类统计操作。
- 数组长度始终是 2 的幂(如 2、4、8…),便于用位运算快速取模
- 初始化不是一上来就分配,而是等到第一次 CAS 更新
baseCount失败时才触发 - 每个线程通过自己的
ThreadLocalRandom.getProbe()计算散列索引,尽量落到不同槽位
计数过程怎么避开竞争
每次 put() 或 remove() 成功后,都会调用 addCount() 尝试更新总数。它的策略是分层尝试:
- 先尝试直接 CAS 修改
baseCount—— 低并发时这一步就完成了 - 如果失败(说明有竞争),再查
counterCells数组是否已存在;若不存在,就初始化它 - 若数组存在,就用当前线程 probe 值定位一个槽位;如果该槽为空,就新建
CounterCell并写入;如果非空,就尝试 CAS 更新它的value - 若再次 CAS 失败(冲突),才进入
fullAddCount执行扩容或重试逻辑
size() 返回的值为什么不是精确的
size() 调用的是 sumCount(),它只是对当前可见的 baseCount 加上所有非 null 的 CounterCell.value 做一次快照累加:
- 读取过程无锁,也不阻塞写入 —— 某个
CounterCell可能在你读它的一瞬间被另一个线程修改 - 数组本身可能正在扩容,部分新旧槽位状态未完全同步
- 某些线程可能还在更新
baseCount,而你已经读完了它 - 所以返回值是近似值,适合监控和判断阈值,但不适合强一致性校验
为什么最大只扩到 CPU 核心数
CounterCell 数组不是越大越好。设计者限制其长度上限为当前机器的 CPU 核心数,原因很实际:
- 超过核心数后,并发写入的线程数已远大于可用硬件并行能力,再增加槽位只会浪费内存
- 哈希冲突概率随长度增长而下降,但收益递减;在核心数级别已能将冲突率压到很低
- 避免数组过大导致遍历
sumCount()时缓存不友好、耗时变长










