concurrenthashmap 的 size() 方法并非绝对无锁,而是采用分段统计与 volatile 协作的乐观策略:先读 basecount,再累加 countercells 数组值,整体无阻塞但极端竞争下可能触发短暂轻量同步。

ConcurrentHashMap 的 size() 方法并非完全“无锁”,而是采用一种**尽量减少锁竞争、避免全局加锁**的策略,在大多数场景下不阻塞其他线程的读写操作,但内部仍可能触发轻量级同步或 CAS 重试——它追求的是高并发下的**近似实时性与低开销平衡**,而非绝对无锁。
核心思路:分段统计 + volatile 协作
Java 8+ 的 ConcurrentHashMap 废弃了分段锁(Segment),改用更细粒度的 Node 数组 + CAS + synchronized(仅锁定单个链表头或红黑树根节点)。size() 的实现不再遍历整个 table,而是依赖一个关键字段:
-
baseCount:一个 volatile long 字段,记录未发生竞争时的元素增减(如 put 成功且无需加锁时,直接 CAS 更新) -
CounterCell[] counterCells:当多个线程同时更新baseCount发生 CAS 冲突时,会初始化该数组,将计数分散到不同 cell 中(类似 LongAdder 的思想),每个线程只更新自己的 cell,大幅降低冲突
size() 的实际执行流程
调用 size() 时,它会:
- 先读取 volatile 的
baseCount - 再遍历
counterCells数组(如果非 null),累加所有非空 cell 的值 - 两者相加得到最终结果
- 整个过程不加锁、不阻塞其他线程的 put/remove,也不要求 table 状态静止
注意:这个值是调用瞬间的近似快照,因为统计过程中其他线程可能仍在修改;它不保证强一致性,但比全局加锁遍历(如 Hashtable.size())性能高出数个数量级。
为什么不是“绝对无锁”?
虽然 size() 本身不显式加锁,但其依赖的计数更新逻辑在极端竞争下仍涉及:
- CAS 失败后可能触发
fullAddCount(),其中对counterCells数组的扩容或初始化会使用 synchronized 锁住整个 CounterCell 数组(但锁粒度极小、时间极短) - 某些 JDK 版本中,若
counterCells为 null 且初始化失败,会退回到尝试 CAS 更新baseCount,仍属无锁路径
因此更准确的说法是:size() 是乐观、非阻塞、基于分治计数的高效实现,绝大多数情况下无锁,仅在极少数竞争升级时有短暂轻量同步。
开发者注意事项
不要依赖 size() 做条件判断(如 if (map.size() == 0) {...}),因为它不是原子快照。正确做法是:
- 用
isEmpty()(内部也是基于 baseCount 和 counterCells 快速判断,语义更明确) - 需要强一致性逻辑时,自行加锁或使用其他同步机制
- 理解 size() 返回的是统计时刻的“最佳尽力值”,适合监控、日志、估算,不适合精确协调
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











