ConcurrentHashMap 高频读写性能优势源于读操作无锁、写操作仅锁单个桶、扩容不阻塞且线程安全;get 依赖 volatile 保证可见性,put 锁粒度降至桶级,扩容通过 ForwardingNode 协作完成。

ConcurrentHashMap 在高频读写下性能优势的核心,不是“它比 HashMap 快”,而是“它让读几乎不阻塞、写只锁极小范围,且不因扩容崩掉”。
为什么 get 操作基本不加锁
ConcurrentHashMap 的 get 方法全程没有 synchronized 或 lock,靠的是 volatile 修饰的节点字段(如 Node.val、Node.next)保证可见性。只要 key 的 hash 定位到桶后首节点匹配,就直接返回——这个过程是无锁、无竞争、可并行的。
常见错误现象:有人以为“线程安全就得锁住读”,结果在读多场景硬套 synchronizedMap,吞吐量断崖下跌。
- 读操作不阻塞其他读,也不阻塞其他写(除非写恰好修改同一桶)
- 即使正在扩容,
get遇到ForwardingNode也会跳转到新表继续查,不会卡住或报错 - 注意:弱一致性意味着刚
put的值,可能短暂不被其他线程的get看到,但不会读到脏数据或抛异常
put 操作锁粒度到底有多小
JDK 1.8 后,ConcurrentHashMap 已经没有 Segment,锁的单位降到了单个桶(即 Node[] 数组某个索引位置)。只有当多个线程同时向同一个桶插入(hash 冲突)时,才可能竞争同一把 synchronized 锁(锁在该桶的首节点上)。
使用场景:比如用用户 ID 做 key,ID 分布均匀,那 99% 的写操作根本碰不到同一把锁。
- 如果桶里是链表,锁首节点;如果是红黑树,锁树的根节点
- 扩容时的
transfer是分段迁移,每个线程负责一部分桶,不是全表阻塞 - 对比
Hashtable:所有put/get都抢同一把对象锁,QPS 随并发线程数增长几乎为零
扩容不导致死循环或数据丢失
这是它和 JDK 7 HashMap 最关键的分水岭。老版 HashMap 并发 resize 会因头插法成环,CPU 100%;JDK 8 HashMap 改尾插但仍可能丢数据;而 ConcurrentHashMap 的扩容是协作式、状态驱动的:
- 用
sizeCtl控制扩容触发与参与线程数 - 每个桶迁移完成后打标记(
ForwardingNode),后续读写自动路由 - 迁移中任何线程的
put都能正常落到新旧表,不会覆盖或静默失败 - 实际压测中,即使每秒数千次 put + 数万次 get,也极少出现扩容卡顿
容易被忽略的三个实操细节
性能优势不是默认生效的,几个配置和用法偏差会让效果大打折扣:
- 初始化容量别设太小——
new ConcurrentHashMap(16)在高写入下会频繁扩容,建议预估 size 后按 1.5 倍向上取最近 2 的幂 - 避免在循环里反复调用
containsKey+get,改用computeIfAbsent或putIfAbsent减少重复 hash 和查找 -
key的hashCode必须合理分布,否则大量 key 落到同一桶,锁竞争立刻上升,性能退化成“伪并发”
真正决定它快不快的,从来不是 API 多炫酷,而是你有没有让它避开锁争用、躲开扩容风暴、喂给它足够散列的 key。


















