JDK 1.8 对 HashMap 除了红黑树还进行了哪些改动?
- 标签:Java 集合、Java
🏛 业务架构师
除了红黑树,JDK 1.8 对 HashMap 还做了几处对高数据量业务有实际价值的改动:
- hash 扰动简化:1.7 是四次移位加异或的复杂扰动,1.8 简化为一次高低 16 位异或。因为树化兜底之后,分布均匀性的压力小了,用更便宜的计算换吞吐,这是成本与效果的再平衡;
- 扩容迁移优化:1.8 利用容量是 2 的 n 次方的特性,扩容后元素要么留在原位置、要么移动"旧容量"这个固定偏移,靠一个 bit 判断即可拆成高低两条链,不再需要重新计算 hash。数据量越大,这个优化省下的迁移时间越可观——大促前的缓存重建、批处理任务里直接受益;
- 数据结构重构:Entry 改为 Node,为树化做铺垫;
- 树化与退化双阈值:链表长度到 8 树化,删到 6 就退化回链表,用滞后阈值避免在边界上反复树化/退化抖动。
从业务视角总结:这些细节都在做同一件事——让大容量、高吞吐场景下的延迟更平稳。做容量规划和性能预算时,理解这些机制能帮我们判断"毛刺来自容器本身还是业务逻辑",避免误判优化方向。
🏗 系统架构师
除了红黑树,JDK 1.8 对 HashMap 我会点出这几个架构级改动:
- 扩容优化:1.7 整表 rehash 重新计算下标;1.8 利用 capacity 为 2 的幂的特性,看 hash 在 oldCap 那一位是 0 还是 1,把链表拆成"留原位"和"原位 + oldCap"两条,且保持相对顺序,尾插也消除了 1.7 并发扩容成环的问题;
- 节点角色统一:1.7 是 Entry 数组 + 独立的 Entry 头表,1.8 改成 Node 数组,TreeNode 也继承 Node,树化和退化时节点可原地转换;
- hash 扰动简化:1.7 做了 4 次移位 + 5 次异或,1.8 只做一次高低 16 位异或,在插入速度、开销和分布质量之间取了更务实的平衡;
- 序列化改进:capacity 按需计算存储。
我的看法:这是一次典型的"热点路径做减法"的性能重构——扰动变便宜、扩容变便宜、最坏情况被红黑树兜底,整套改动都在为高吞吐服务。
💻 全栈开发专家
除了红黑树,JDK 8 对 HashMap 还有几处关键改动,都是我读源码时印象深的:
- 扩容迁移算法重写:1.7 要对每个节点重新 rehash;1.8 只要看
hash & oldCap这一位是 0 还是 1,节点要么留在原索引,要么挪到"原索引 + oldCap",把一条链按高低位拆成两条,且保持相对顺序。 - 插入方式从头插改为尾插:1.7 头插在并发扩容时会形成环形链表导致 get 死循环 CPU 100%(经典线上事故);1.8 尾插解决了这个,但并发 put 仍会丢数据,不能因此认为它线程安全。
- hash 扰动函数:高 16 位异或低 16 位,让高位也参与定位,减少小容量时的碰撞。
- 容量计算
tableSizeFor:构造时传 1000,自动扩成 1024,配合负载因子提前算好,减少运行中扩容。 - 简化数据结构:Entry 改名 Node,树化时才转 TreeNode,普通场景省内存。
实际优化启发:能预估容量就初始化时指定 new HashMap<>(expectedSize / 0.75f + 1),避免反复扩容,这在解析大 JSON 时性能提升明显。