ConcurrentHashMap 的 size() 返回估算值,因采用 baseCount 与非原子读取的 CounterCell 数组实现,无法保证强一致性;其设计以吞吐优先,误差受写入模式影响,业务需依语义选用 isEmpty() 或外部原子计数器。

ConcurrentHashMap 的 size() 返回的是估算值,不是实时精确值,核心原因在于:它必须在不阻塞写操作的前提下完成统计,而高并发场景下,元素数量本身就在持续变化。
计数机制天然无法锁定全部状态
ConcurrentHashMap 不用全局锁或遍历全表的方式统计,而是依赖 baseCount 加上一组分散的 CounterCell 值。这些值由多个线程异步更新,size() 只做一次非原子的快照读取:
- 读取
baseCount时,可能已有线程刚完成 CAS 更新但尚未反映到后续读取中 - 遍历
counterCells数组时,某个Cell可能正被其他线程修改,本次读到的是旧值或新值,无法保证一致性 - 若数组正在扩容(如从长度 2 扩到 4),遍历可能跳过新分配的槽位,或重复读取迁移中的数据
设计目标是吞吐优先,不是强一致
Java 并发容器的设计哲学明确把写入性能和读取响应放在首位。如果每次 size() 都要暂停所有写入、加锁遍历全部桶,那它的并发优势就彻底丧失了:
- 在批量导入、高频上报等场景中,
put()每秒可达数万次,此时精确统计成本远高于业务价值 -
LongAdder引入的分段计数,本质就是用“允许误差”换“避免自旋争抢”,这是对硬件缓存行、CPU 核心数、CAS 失败率等底层因素的务实妥协 - 监控大盘、告警阈值、采样开关等用途,需要的是趋势和量级,不是原子快照
真实误差往往有规律可循
误差不是随机漂移,而是受写入模式影响明显:
- 短时突发写入(如秒杀开售):大量计数暂存在
baseCount,counterCells来不及扩容分流,size()易偏低 - 长稳高并发(如网关会话缓存):
counterCells已稳定扩容至接近 CPU 核心数,各线程基本落到不同槽位,误差通常控制在千分之几以内 - 混合读写密集(如实时风控规则匹配):读线程可能看到部分已删除但未同步到计数器的节点,导致
size()短暂偏高
替代方案取决于业务语义
若真需要精确值,不能靠 size(),而要看场景选合适手段:
- 限流判断(如“不超过 1000 个活跃连接”):用
size()完全够用,误差不影响策略有效性 - 资源清理条件(如“map 为空才关闭连接池”):应改用
isEmpty()——它只检查首个桶是否为 null,开销极小且语义更可靠 - 事务性校验(如库存扣减前确认总数):必须配合外部原子计数器(如
AtomicInteger),并在所有put/remove路径严格同步增减










