MST

星途 面试题库

面试题:Java HashMap扩容时如何重新分配元素以及对性能优化的考虑

当Java HashMap进行扩容时,元素是如何重新分布到新的桶数组中的?在这个过程中,HashMap的设计者做了哪些性能优化的考虑?如果在高并发场景下,HashMap的扩容机制可能会引发什么性能问题?
28.7万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

元素重新分布

  1. 计算新位置:在Java中,HashMap扩容时,会创建一个新的更大的桶数组。对于原数组中的每个元素,会重新计算其在新数组中的位置。其计算方式是基于元素的哈希值和新数组的长度。
    • 假设原数组长度为oldCap,新数组长度为newCapnewCap = oldCap * 2)。原数组中某个元素的哈希值为hash,在原数组中的索引为index = hash & (oldCap - 1) ,在新数组中的索引为newIndex = hash & (newCap - 1)
    • 由于newCap = oldCap * 2,所以newCap - 1oldCap - 1多了一个高位1。这就导致hash & (newCap - 1)的结果要么与hash & (oldCap - 1)相同(当hash的新增高位为0时),要么比hash & (oldCap - 1)oldCap(当hash的新增高位为1时)。
  2. 链表或红黑树的迁移:如果桶中存储的是链表,会遍历链表,将每个节点重新计算位置后放入新数组对应位置。如果桶中存储的是红黑树,会将红黑树中的节点重新计算位置后,根据新位置分布到新数组的不同桶中,并且在合适的情况下(节点数量小于TREEIFY_THRESHOLD)可能会退化为链表。

性能优化考虑

  1. 减少重新计算哈希值:利用hash & (newCap - 1)hash & (oldCap - 1)的关系,使得大部分元素在扩容时不需要重新计算哈希值,仅通过新增高位的判断就能确定新位置,减少计算开销。
  2. 空间和时间平衡:扩容时机的选择是一种平衡,扩容过早会导致空间浪费,扩容过晚则会使哈希冲突加剧影响性能。HashMap通过负载因子(默认0.75)来控制扩容时机,在空间和时间性能上达到较好的平衡。

高并发场景下的性能问题

  1. 数据丢失或死循环:在多线程环境下,当多个线程同时进行扩容操作时,可能会出现数据丢失的情况。因为在扩容过程中涉及到链表的迁移,多线程操作可能导致链表结构被破坏,从而丢失数据。同时,在JDK 1.7及之前的版本中,还可能出现死循环问题。由于多线程下链表迁移操作的交织,可能会使链表形成环形结构,在后续访问链表时陷入死循环。
  2. 竞争加剧:高并发场景下,多个线程同时访问和修改HashMap,尤其是在扩容时,会导致竞争加剧,从而降低系统的并发性能。例如,多个线程同时往新数组的同一个桶中添加元素,会频繁地进行锁竞争(如果没有合适的并发控制)。