concurrenthashmap put不阻塞整个map是因为锁粒度降至node级别,仅锁桶首节点;get全程无锁但可能读到过期数据;遍历时为弱一致性快照,非强一致。

ConcurrentHashMap put操作为什么不会阻塞整个map
因为JDK 1.8里它把锁粒度从“分段锁”降到了“Node级别”,实际是锁住链表头节点或红黑树根节点,而不是整个桶数组。put时先用casTabAt尝试无锁插入;失败才用synchronized锁住对应桶的首节点——这意味着16个线程往不同桶写,几乎互不干扰。
常见错误现象:ConcurrentHashMap在高并发下仍出现明显性能抖动,往往是因为大量哈希冲突导致多个键落到同一桶,被迫串行化执行。
- 哈希函数质量差(比如自定义
hashCode总返回固定值)会加剧单桶竞争 - 初始容量设太小(默认16),扩容前容易桶满,触发链表转红黑树,反而增加同步开销
-
computeIfAbsent这类复合操作仍需加锁,不能假设“所有操作都无锁”
get操作真的完全不加锁吗
是的,get全程无锁,靠的是volatile读 + Unsafe的getObjectVolatile保证可见性。但要注意:它不保证实时性——可能读到“已删除但还未完成清理”的中间状态(比如正在转移的节点)。
使用场景:适合读多写少、能容忍短暂stale read的缓存场景;不适合强一致性要求的计数器或状态机。
-
get不阻塞任何写操作,但写操作中若发生扩容(transfer),get可能跨新旧table读取,逻辑上仍正确 - 不要在
get结果上做条件判断后直接put(如“如果key不存在则put”),这会产生竞态,应改用computeIfAbsent - 对value对象本身做修改(如
get(key).add(item))是线程安全的,但前提是value类型自己线程安全
CAS失败后为什么不是立即重试而是先帮扩容
当put发现当前桶正在被其他线程扩容(即tab[i]是ForwardingNode),它不会傻等或自旋,而是主动调用helpTransfer去协助迁移数据——这是JDK 1.8的关键优化:把“等待”变成“协作”,缩短整体扩容耗时。
性能影响:单次put延迟可能略增(要多跑一段迁移逻辑),但全局吞吐显著提升,尤其在写压力持续时。
- 扩容阈值由
sizeCtl控制,不是简单看size >= capacity * loadFactor,还涉及并发计数的误差补偿 - 协助扩容的线程不会全量搬完,只搬分配给它的步长(默认一个桶),避免单线程卡死
- 如果当前线程刚帮完忙就立刻
put成功,它不会重复参与下次扩容——sizeCtl的更新和校验保证了这点
为什么不能用foreach遍历ConcurrentHashMap
因为entrySet().iterator()返回的是弱一致迭代器(weakly consistent),不抛ConcurrentModificationException,但也不保证看到所有元素或不重复——它基于遍历时的table快照,期间发生的增删改可能被跳过或重复。
容易踩的坑:用for (Entry e : map.entrySet())做统计或校验,结果偶尔对不上;或者在遍历中调用remove,行为不可预测(可能漏删、多删,甚至NPE)。
- 需要强一致性遍历时,改用
mappingCount()+forEach方法(JDK 8+),它是按当前状态批量处理的 -
keySet().stream()同理弱一致,别依赖其精确性 - 真要边遍历边删,必须用
Iterator.remove(),且仅限当前元素,不能跳着删
最常被忽略的一点:很多人以为“Concurrent”=“遍历安全”,其实它只保障单个操作原子性,迭代过程本身仍是快照语义——这和CopyOnWriteArrayList的思路完全不同,别混用场景。










