ConcurrentHashMap put 流程
面试官问:"ConcurrentHashMap 的 put 方法是怎么实现的?"
候选人小张答:"先用 CAS 插入,失败了再用 synchronized。"
面试官追问:"具体是怎么判断的?CAS 和 synchronized 分别在什么情况下使用?"
小张支支吾吾答不上来。
【面试官心理】 put 流程是理解 ConcurrentHashMap 的核心。能够说清楚 CAS 和 synchronized 分别在什么情况下使用的候选人,说明真正理解了 ConcurrentHashMap 的并发设计。
一、put 方法入口 🔴
1.1 公开方法
1.2 put 流程图
二、spread 方法(哈希扰动)🔴
2.1 spread 实现
2.2 为什么需要 spread
三、CAS 插入 🔴
3.1 桶为空时的插入
3.2 tabAt 和 casTabAt
tabAt 使用 volatile 读取,保证看到其他线程的最新写入。casTabAt 使用 CAS,保证插入的原子性。
3.3 CAS 失败重试
四、synchronized 锁桶 🟡
4.1 为什么需要 synchronized
4.2 synchronized 块内部
4.3 ❌ 错误示范
候选人原话:"ConcurrentHashMap 用 CAS 保证线程安全,所以不需要锁。"
问题诊断:
- 忽略了 synchronized 在链表/红黑树操作中的作用
- 不理解 CAS 在高竞争下的失败重试
- 不理解为什么需要 synchronized
面试官内心 OS:"这个候选人可能只是看过表面,没有深入理解并发场景的复杂性。"
【面试官心理】 ConcurrentHashMap 的 put 流程中,CAS 用于初始化和简单插入,synchronized 用于链表/红黑树的修改。两者结合,既保证了性能,又保证了线程安全。
五、红黑树插入 🟡
5.1 TreeBin 的特殊处理
5.2 TreeBin vs TreeNode
六、树化阈值检查 🟡
6.1 树化条件
6.2 treeifyBin 方法
七、计数更新 🟡
7.1 addCount 方法
7.2 sizeCtl 的作用
八、协助扩容 🟡
8.1 扩容检测
8.2 helpTransfer 方法
当一个线程发现其他线程正在扩容时,它会协助扩容,而不是等待。这是 ConcurrentHashMap 的一个优化,减少了扩容的停顿时间。
九、并发场景分析 🟡
9.1 线程 A 和线程 B 同时 put 到同一个桶
9.2 线程 A put,线程 B get
9.3 volatile 保证可见性
十、面试高频追问 🟡
10.1 第一层追问
面试官:"ConcurrentHashMap 的 put 方法什么时候用 CAS,什么时候用 synchronized?"
候选人:...
正确回答:
- CAS:桶为空时尝试插入
- synchronized:桶不为空时(链表/红黑树的插入、更新、删除)
10.2 第二层追问
面试官:"synchronized 只锁住一个桶,会不会影响其他线程操作其他桶?"
候选人:...
正确回答:不会。synchronized 只锁住一个桶(链表头),其他线程可以同时操作不同的桶,实现真正的并发。
10.3 第三层追问
面试官:"ConcurrentHashMap 的 key/value 能为 null 吗?"
候选人:...
正确回答:不能。如果为 null,put 方法会抛出 NullPointerException。这是和 HashMap 的主要区别之一。
10.4 第四层追问
面试官:"ConcurrentHashMap 的 size() 方法是怎么工作的?"
候选人:...
正确回答:
- 使用 CounterCell 分散计数
- 多个线程并发更新不同的 CounterCell
- 累加时遍历所有 CounterCell 求和
- 不需要加锁,性能好
【学习小结】 ConcurrentHashMap put 流程要点:
- spread() 计算哈希(扰动 + 保证正数)
- 桶为空 → CAS 插入
- 桶为 ForwardingNode → 协助扩容
- 桶为普通节点 → synchronized 锁桶,链表/红黑树操作
- binCount >= 8 → 树化
- addCount() 更新计数,可能触发扩容