面试题答案
一键面试元素重新分布
- 计算新位置:在Java中,
HashMap
扩容时,会创建一个新的更大的桶数组。对于原数组中的每个元素,会重新计算其在新数组中的位置。其计算方式是基于元素的哈希值和新数组的长度。- 假设原数组长度为
oldCap
,新数组长度为newCap
(newCap = oldCap * 2
)。原数组中某个元素的哈希值为hash
,在原数组中的索引为index = hash & (oldCap - 1)
,在新数组中的索引为newIndex = hash & (newCap - 1)
。 - 由于
newCap = oldCap * 2
,所以newCap - 1
比oldCap - 1
多了一个高位1。这就导致hash & (newCap - 1)
的结果要么与hash & (oldCap - 1)
相同(当hash
的新增高位为0时),要么比hash & (oldCap - 1)
大oldCap
(当hash
的新增高位为1时)。
- 假设原数组长度为
- 链表或红黑树的迁移:如果桶中存储的是链表,会遍历链表,将每个节点重新计算位置后放入新数组对应位置。如果桶中存储的是红黑树,会将红黑树中的节点重新计算位置后,根据新位置分布到新数组的不同桶中,并且在合适的情况下(节点数量小于
TREEIFY_THRESHOLD
)可能会退化为链表。
性能优化考虑
- 减少重新计算哈希值:利用
hash & (newCap - 1)
和hash & (oldCap - 1)
的关系,使得大部分元素在扩容时不需要重新计算哈希值,仅通过新增高位的判断就能确定新位置,减少计算开销。 - 空间和时间平衡:扩容时机的选择是一种平衡,扩容过早会导致空间浪费,扩容过晚则会使哈希冲突加剧影响性能。
HashMap
通过负载因子(默认0.75)来控制扩容时机,在空间和时间性能上达到较好的平衡。
高并发场景下的性能问题
- 数据丢失或死循环:在多线程环境下,当多个线程同时进行扩容操作时,可能会出现数据丢失的情况。因为在扩容过程中涉及到链表的迁移,多线程操作可能导致链表结构被破坏,从而丢失数据。同时,在JDK 1.7及之前的版本中,还可能出现死循环问题。由于多线程下链表迁移操作的交织,可能会使链表形成环形结构,在后续访问链表时陷入死循环。
- 竞争加剧:高并发场景下,多个线程同时访问和修改
HashMap
,尤其是在扩容时,会导致竞争加剧,从而降低系统的并发性能。例如,多个线程同时往新数组的同一个桶中添加元素,会频繁地进行锁竞争(如果没有合适的并发控制)。