ConcurrentHashMap禁null是为消除并发下get返回null的二义性:无法区分key不存在还是value为null,且containsKey与get非原子操作,禁止null从源头确保语义唯一。

大厂面试对 ConcurrentHashMap 的追问,从来不是考你会不会用,而是看你能不能讲清“为什么这么设计”。核心就三点:锁怎么分的、扩容怎么不卡顿、null 为什么不让存。下面直接拆解最常被连环追问的几个点。
锁粒度怎么一步步变细的?
面试官爱问这个,是想看你是否理解并发优化的演进逻辑。
- JDK 1.7 是 Segment 分段锁:默认 16 个 Segment,每个 Segment 是一个独立的 ReentrantLock + HashEntry 数组。线程操作不同段完全不冲突,但 Segment 数量固定、内存开销大、扩容要锁全表。
- JDK 1.8 彻底去掉 Segment:改用“CAS + 桶级 synchronized”。插入空桶用 CAS 无锁完成;非空桶则只锁链表头节点或红黑树根节点。同一数组里,十个桶可以十个线程同时写,互不影响。
- 注意误区:ConcurrentHashMap 不是“完全无锁”,而是“尽量少锁、锁得短、锁得小”。synchronized 锁的是单个 Node,不是整个 Map。
扩容时怎么做到不阻塞读写?
这是区分背八股和真懂源码的关键题。重点不在“扩”,而在“怎么边扩边用”。
- 触发扩容后,不是等一个线程干完所有迁移再开放服务。而是用 sizeCtl 控制状态,多个线程可协作搬运数据。
- 旧数组里的每个桶,由第一个访问它的线程负责迁移;后续线程看到 ForwardingNode(迁移标记节点),就自动去帮着搬其他桶,实现任务分片。
- 读操作遇到 ForwardingNode,会直接转向新数组查找;写操作遇到它,则先帮着迁移当前桶,再执行 put。整个过程没有全局停顿(STW)。
为什么 key 和 value 都不能为 null?
这个问题表面问限制,实际考你对并发语义的理解。
立即学习“Java免费学习笔记(深入)”;
- 如果允许 value 为 null,get(key) 返回 null 时,无法区分是 key 不存在,还是 key 存在但值就是 null——这种二义性在并发环境下无法靠加锁消除。
- key 为 null 更麻烦:hash 计算会出问题(key.hashCode() 报 NPE),且 null 的 hash 值固定为 0,容易集中到同一个桶,破坏散列均匀性。
- 所以干脆禁止,从源头杜绝歧义。这不是偷懒,而是设计上的明确契约。
链表转红黑树的阈值为什么是 8?
别只答“泊松分布”,要说清工程权衡。
- 实验表明,当哈希分布理想时,桶中元素个数服从参数 λ=0.5 的泊松分布,链表长度 ≥8 的概率已低于千万分之一。
- 选 8 是平衡点:再小(如 6),树化太频繁,额外开销大;再大(如 10),链表过长导致查询退化成 O(n),影响高并发下的响应稳定性。
- 还有前提:必须数组长度 ≥64 才树化。否则优先扩容——说明设计者把“扩容”看作更轻量的优化手段,树化是兜底方案。


















