SWE · VO System Design / Concurrency Follow-up

Databricks ConcurrentHashMap:为什么普通 HashMap 不安全、锁要保护什么

从丢更新和扩容竞态出发,比较全局锁、分段锁与细粒度桶锁,明确复合操作的原子性边界。

题目重点:从丢更新和扩容竞态出发,比较全局锁、分段锁与细粒度桶锁,明确复合操作的原子性边界。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 讨论通用 hash map 设计:get/put/remove,键哈希稳定;是否允许 null、弱一致 iterator、computeIfAbsent、resize 策略都须另问。
  • 线程安全目标先定为单操作线性化,复合 read-modify-write 仅在单个原子 API 下保证;不同语言内存模型实现细节需对应验证。

解题路线

  1. 普通 hash map 的两个 put 可能同时写同一桶而丢更新;读线程可能看到写入中的节点/桶或扩容后的中间状态,且没有内存可见性保证。问题不只是‘两个线程同时改值’,也包括结构不变量与发布顺序。
  2. 最简单正确基线是一把 mutex 包住所有 get/put/remove/resize,线性化点在锁内实际变更或读取;实现易审计但热读写串行。读多可考虑 reader-writer lock,不过写饥饿与锁升级要处理。
  3. 提高并发度可按 hash 分段:每段独立桶表和锁,操作只锁其段;段内 resize 只动该段,避免全表迁移要停所有段。另一方案桶锁/无锁读更复杂,需要安全发布、节点版本、迁移标记和内存回收,不应口头称 lock-free 就结束。
  4. `get` 返回值后再 `put` 的两个调用之间可被其他线程修改,因此 `increment` 或 `putIfAbsent` 应提供在同一锁范围内的原子复合 API。跨多个 key 的事务即使分段锁也要固定锁顺序防死锁。
  5. 验证包括同 key 并发递增、不同 key 并发、resize 期间读写、删除后重插、迭代与异常路径;吞吐用热点键与均匀键分布分别测,不能只报平均 QPS。

手动走一遍

counter=0,线程 A/B 都 get→0,各自 put→1,最终是 1 而不是 2;即使用线程安全 map,两个独立调用仍会丢更新。若提供 compute(key, old→old+1) 并锁住该 key 的段,最终才是 2。

规模与取舍

无竞争哈希操作期望 O(1),碰撞最坏依桶结构;全局锁使并行吞吐受单临界区限制。S 段锁空间 O(S),均匀哈希下可并行操作不同段,热点键仍单点串行;resize 依迁移数据量 O(N)。

易错点与追问

容易失分的边界

  • 把普通 HashMap 的无同步并发读写说成只会丢一个值。
  • 以为线程安全容器自动保证 get-then-put 复合原子。
  • 忽略 resize/rehash 时读写的中间态。
  • 跨段多锁没有固定顺序。
  • 声称 Java 某版本固定使用一种锁结构,忽略版本差异。

继续深挖

  • 读多写少时一把 mutex 与 RWLock 哪个更好?
  • 分段锁怎样安全 resize?
  • computeIfAbsent 的回调能否阻塞/重入?
  • 迭代器要求快照还是弱一致?
  • 两个 key 同时更新怎样避免死锁?
✓已复制微信号!