concurrenthashmap 的 size() 方法采用“分段计数 + 乐观重试”策略,通过 basecount 和 volatile countercell[] 数组实现无锁、高并发的近似准确计数,误差通常为 0 或 ±1。

ConcurrentHashMap 的 size() 方法不通过全局加锁来统计元素个数,而是采用“分段计数 + 乐观重试”的策略,在大多数情况下避免阻塞,兼顾准确性与并发性能。
基于 counterCells 数组的分段计数
ConcurrentHashMap 内部维护一个 transient volatile CounterCell[] counterCells 数组,每个线程在执行 put、remove 等修改操作时,会尝试将计数增量写入该数组的某个槽位(通过线程本地的 probe 值哈希定位),而不是直接更新共享的 baseCount。这种设计把竞争分散到多个单元,减少冲突。
- 初始时只用
baseCount字段记录总数,无竞争时无需数组 - 发生 CAS 失败(说明有竞争)后,才初始化
counterCells并扩容 - 每个线程使用
ThreadLocalRandom派生的 probe 值定位槽位,降低哈希冲突概率
size() 方法的三步乐观读取逻辑
调用 size() 时,并不加锁,而是执行以下步骤:
- 先读取
baseCount作为基础值 - 再遍历
counterCells数组,累加所有非 null 槽位的value - 如果遍历过程中发现数组正在扩容(
cellsBusy == 1)或结构变化(如 sizeCtl 被修改),就重试一次——最多重试两次,之后退化为对所有 bin 进行遍历计数(仍无锁,但成本更高)
这种“先快后准”的方式,保证了高并发下多数调用能快速返回近似准确值(误差通常为 0 或 ±1),极少数场景才付出更高代价确保精确。
为什么不用全局锁或 volatile long?
全局锁违背 ConcurrentHashMap 的并发设计目标;而仅靠一个 volatile long 字段无法解决多线程同时 increment 的竞态问题(缺少原子自增语义)。CAS + 分段数组是在无锁前提下实现可伸缩计数的成熟方案,类似 LongAdder 的思想,被直接复用于 ConcurrentHashMap。
-
CounterCell是一个简单的静态内部类,仅含 volatile long value - 所有写操作都用
Unsafe.compareAndSwapLong更新对应槽位 - 读操作全程无同步块,依赖 volatile 语义保证可见性
实际使用注意事项
尽管 size() 尽量高效,但它返回的是某一时刻的快照值,不是严格实时的:
- 高并发写入时,两次连续调用可能得到不同结果,这是正常现象
- 若需强一致性判断(如“是否为空”),推荐用
isEmpty(),它只检查baseCount == 0 && counterCells == null || 所有 cell.value == 0,开销更低 - 避免在循环条件中频繁调用
size()做判断,可考虑用迭代器或批量操作替代
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











