核心内容摘要
深夜福利社:那些让人面红耳赤的色色动画推荐,热门动漫合集全攻略
好的我们来深入分析HashMap的核心机制重点关注从 JDK
7 到
8 的重大改进特别是解决死循环问题和引入高低位映射优化。
JDK
7HashMap的结构与潜在问题在 JDK
7 中HashMap采用数组 链表的结构数组 (table): 存储链表的头节点。
链表 (Entry): 解决哈希冲突相同哈希值的元素存储在同一个链表中采用头插法新元素插入链表头部。
// JDK
7 简化版 Entry 结构 static class EntryK, V implements Map.EntryK, V { final K key; V value; EntryK, V next; // 指向下一个节点的指针 int hash; // ... 构造方法等 }
1 并发扩容导致的死循环问题当HashMap需要扩容通常是当size threshold capacity * loadFactor时会执行resize()操作创建新数组: 容量翻倍。
迁移元素: 遍历旧数组的每个桶链表重新计算每个元素在新数组中的位置index (newCapacity -
hash。
迁移方式: 使用头插法将元素插入新数组的对应链表中。
问题根源 - 头插法与并发在并发环境下多个线程可能同时触发resize()。
头插法会导致链表元素的迁移顺序发生反转旧链表 A-B-C迁移后在新链表变成 C-B-A。
如果两个线程同时迁移同一个链表由于线程执行顺序的不确定性可能导致链表形成环形结构。
// 简化版 JDK
7 resize 中的迁移逻辑 (易产生死循环) void transfer(Entry[] newTable) { for (EntryK, V e : oldTable) { while (null ! e) { EntryK, V next e.next; // 线程1执行到这里暂停 e.next newTable[e.hash (newCapacity -
]; // 头插法 newTable[e.hash (newCapacity -
] e; e next; } } }线程1执行EntryK, V next e.next;后暂停。
假设此时e指向节点 Anext指向节点 B。
线程2完成了整个链表的迁移假设迁移后链表是 B-A头插法导致顺序反转。
线程1恢复执行e.next newTable[index];将 A 的next指向新数组index位置的节点此时是 B因为线程2迁移后 B 是头节点。
newTable[index] e;将 A 设置为新数组index位置的头节点。
现在链表是 A-B。
e next;e指向 B。
下一轮循环next e.next B.next。
在线程2迁移后的链表中B 的next指向 A所以next指向 A。
e.next newTable[index];将 B 的next指向新数组index位置的头节点此时是 A。
现在链表变成 B-A-B环形链表形成后续调用get()访问这个桶时遍历链表会陷入死循环。
JDK
8HashMap的重大优化JDK
8 对HashMap进行了显著重构数据结构:数组 链表 / 红黑树。
当链表长度超过阈值默认为 8且桶数组长度达到一定大小默认为 64时链表转换为红黑树以提高查询效率$$O(n) \to O(\log n)$$。
迁移方式: 改用尾插法新元素插入链表尾部保持元素顺序不变从根本上解决了并发扩容死循环问题但HashMap本身仍非线程安全。
高低位映射优化: 这是扩容迁移过程中的关键性能优化。
1 高低位映射优化详解在 JDK
8 的resize()方法中迁移元素时不再简单地重新计算每个元素的哈希值(newCapacity -
hash而是利用了一个巧妙的性质元素在新数组中的位置只可能是原位置i或者i oldCapacity。
数学原理设旧容量为oldCap新容量newCap 2 * oldCapHashMap容量总是 2 的幂。
计算桶索引的公式是index (capacity -
hash。
由于capacity是 2 的幂capacity - 1的二进制形式是低位全 1高位全 0例如oldCap 16 (
oldCap - 1 15 (
。
新容量newCap - 1的二进制是比oldCap - 1多一个高位 1例如newCap 32 (
newCap - 1 31 (
。
计算元素在新数组中的位置 $$index_{new} (newCap -
hash (2 \times oldCap -
hash$$比较元素在旧数组中的位置 $$index_{old} (oldCap -
hash$$关键在于hash值在第log2(oldCap)位即oldCap对应的那个二进制位的值如果该位是0则(newCap -
hash和(oldCap -
hash的结果完全相同因为新增的高位在操作中是 0不影响低位结果。
所以元素应留在原位index_{old}。
如果该位是1则(newCap -
hash的结果等于(oldCap -
hash oldCap因为新增的高位是 1后相当于在index_{old}基础上加了一个oldCap。
所以元素应迁移到新位置index_{old} oldCap。
优化实现 JDK
8 在迁移时直接将一个旧桶 (oldTab[j]) 拆分成两个新链表低位链表 (loHead,loTail): 存放哈希值满足(hash oldCap) 0的元素第log2(oldCap)位为 0它们将留在新数组的j位置。
高位链表 (hiHead,hiTail): 存放哈希值满足(hash oldCap) ! 0的元素第log2(oldCap)位为 1它们将迁移到新数组的j oldCap位置。
// JDK
8 resize() 中迁移逻辑的简化 (包含高低位优化) NodeK, V[] newTab (NodeK, V[]) new Node[newCap]; // ... 其他初始化 for (int j 0; j oldCap; j) { NodeK, V e; if ((e oldTab[j]) ! null) { NodeK, V loHead null, loTail null; // 低位链表头尾 NodeK, V hiHead null, hiTail null; // 高位链表头尾 do { NodeK, V next e.next; if ((e.hash oldCap)
{ // 判断关键位是否为0 if (loTail null) loHead e; else loTail.next e; loTail e; } else { // 关键位为1 if (hiTail null) hiHead e; else hiTail.next e; hiTail e; } } while ((e next) ! null); // 将两个链表放入新数组对应位置 if (loTail ! null) { loTail.next null; newTab[j] loHead; // 低位链表留在原位 j } if (hiTail ! null) { hiTail.next null; newTab[j oldCap] hiHead; // 高位链表移到 j oldCap } } }优势:避免重新计算哈希: 只需进行一次位运算(e.hash oldCap)判断关键位即可确定元素的新位置效率远高于重新计算(newCap -
hash。
链表拆分高效: 遍历一次旧链表同时构建两个新链表低位和高位迁移操作非常高效。
保持元素顺序: 使用尾插法构建新链表保持了元素的相对顺序对后续转换为红黑树也有利。
3.
总结对比 (JDK
7 vs JDK
1.
特性JDK
7JDK
8改进点数据结构数组 链表数组 链表 / 红黑树查询效率提升 (长链表 $$O(n) \to O(\log n)$$)冲突解决插入法头插法尾插法解决并发扩容死循环保持顺序扩容迁移方式遍历链表每个元素重新计算位置高低位映射优化链表拆分成低位和高位避免重算哈希迁移效率显著提升线程安全非线程安全 (死循环问题)非线程安全 (但无死循环)更健壮但仍需ConcurrentHashMap保证安全JDK
8 的HashMap通过引入红黑树、尾插法和高低位映射优化在解决
7 致命缺陷死循环的同时显著提升了性能尤其在哈希冲突严重和扩容时是现代 Java 应用中更高效、更可靠的选择。
理解这些底层机制对于编写高性能代码和排查问题至关重要。