真实面经题目 · 原创解析
HashMap 和 ConcurrentHashMap 的原理与区别是什么?
考察 Java 哈希表结构和并发容器实现,重点是桶结构、扩容、线程安全策略、弱一致迭代和 null 限制。
真实面经题目 · 原创解析
考察 Java 哈希表结构和并发容器实现,重点是桶结构、扩容、线程安全策略、弱一致迭代和 null 限制。
HashMap 和 ConcurrentHashMap 都是哈希桶结构,JDK 8 里冲突严重时会从链表树化为红黑树。HashMap 非线程安全,并发 put、扩容和读写交错可能出现数据丢失或结构异常;ConcurrentHashMap 面向并发访问,JDK 8 主要用数组元素的 volatile 可见性、CAS 初始化桶、桶级 synchronized 写入、协助扩容和计数分散来降低竞争。它的读通常不加全局锁,迭代是弱一致视图,并且不允许 null key 或 null value,因为并发下 get 返回 null 必须明确表示不存在。
两者核心都是数组加桶节点,哈希值定位数组下标,冲突时在桶内形成链表;当链表长度超过阈值且数组容量足够时,桶会树化为红黑树,以降低极端冲突下的查询复杂度。
HashMap 的 put、resize 和结构修改没有同步保护,多线程同时写可能覆盖更新、读到中间状态或造成元素丢失。即使 JDK 8 避免了早期扩容环链问题,也不能把 HashMap 当并发容器。
JDK 8 ConcurrentHashMap 不再用固定 Segment 分段锁作为主要结构,而是基于 Node 数组、CAS、volatile 和桶级 synchronized。空桶用 CAS 放入首节点,非空桶写入时锁住桶头,读路径尽量无锁。
ConcurrentHashMap 扩容时多个线程可以协助迁移桶,通过 forwarding node 标识迁移状态;size 计数使用 baseCount 和 counterCells 分散更新,降低高并发下单点计数竞争。
ConcurrentHashMap 的迭代器是弱一致的,遍历时可以容忍并发修改,但不保证看到某个瞬间的完整快照。它不允许 null key 或 null value,避免并发场景下把“key 不存在”和“value 为 null”混淆。
不是传统 JDK 7 那种 Segment 数组分段锁。JDK 8 的主要结构是 Node 数组,写入时在桶级别加锁,并结合 CAS、volatile 和协助扩容。
并发环境下 `get(key)` 返回 null 需要明确表示 key 不存在。如果允许 value 为 null,调用方无法区分是没这个 key,还是 key 存在但值为 null,尤其在并发修改时更危险。
普通 get 依赖 volatile 可见性和节点链路读取,通常不加锁;但遇到正在迁移或树节点等特殊结构会走相应逻辑。它不是完全无锁容器,写入仍可能锁桶。
桶内链表长度达到树化阈值,并且数组容量达到最小树化容量时才会树化;容量太小时优先扩容,因为扩容后冲突可能自然分散。