HashMap 红黑树化阈值
面试官问:"HashMap 什么时候会把链表转成红黑树?"
候选人小何答:"链表长度达到 8 的时候。"
面试官点点头,继续问:"那红黑树什么时候会退链表?"
小何说:"呃... 长度变成 6?"
面试官追问:"为什么是 8 和 6?不是对称的 8 和 8?"
小何彻底答不上来。
【面试官心理】 这道题考查的是候选人对 HashMap 源码的理解深度。能说出阈值的占 60%,能说出为什么非对称的占 20%,能解释泊松分布的只有 5%。这道题是 P6 和 P5 的分水岭。
一、红黑树化阈值 🔴
1.1 JDK 8 源码定义
1.2 树化条件
链表转红黑树需要同时满足两个条件:
- 链表长度
>= TREEIFY_THRESHOLD(8) - HashMap 容量
>= MIN_TREEIFY_CAPACITY(64)
为什么容量小于 64 先扩容?
因为这个时候数据量还不大,扩容可以解决哈希碰撞问题,没必要树化。
二、为什么是 8?🔴
2.1 泊松分布
JDK 源码注释解释了为什么选择 8:
2.2 链表 vs 红黑树的性能拐点
*插入/删除的 O(1) 前提是已知位置,查找位置仍需要 O(n)。
链表和红黑树的性能拐点:
- 链表长度
< 8:链表查找最多 7 次,红黑树 O(log n) = 3 次,链表更快 - 链表长度
= 8:链表查找 8 次,红黑树仍只需 3 次,红黑树开始更快 - 链表长度
> 8:红黑树优势明显
三、为什么退链表阈值是 6 而不是 8?🔴
3.1 避免在阈值边界反复树化/退链表
如果退链表阈值也是 8:
3.2 实际设计
这样设计形成了一个缓冲区:
3.3 代码验证
四、为什么选择红黑树而不是 AVL?🟡
4.1 AVL vs 红黑树
4.2 HashMap 的场景选择
HashMap 是写多读少的数据结构:
- 每次
put都要找位置插入 get操作虽然频繁,但 O(log n) 已经足够快- 插入性能比查找性能更重要
因此选择插入代价更小的红黑树而不是 AVL。
五、面试官追问 🔴
面试官:"如果 HashMap 容量小于 64 但链表很长,会怎么办?"
标准回答:
- 会先触发扩容而不是树化
- 扩容后,链表会重新散列,可能变短
- 只有容量
>= 64且链表长度>= 8才会树化
面试官追问:"JDK 7 和 JDK 8 的 HashMap 有什么区别?"
标准回答:
- JDK 7:数组 + 链表,扩容时可能死循环
- JDK 8:数组 + 链表 + 红黑树,避免了扩容死循环
- JDK 8 还改变了扩容时的 transfer 顺序(非递归、头插改尾插)
面试官追问:"红黑树的查找一定比链表快吗?"
标准回答:
- 不一定。数据量小时,链表(O(n))的常数项更小
- 红黑树(O(log n))的优势在数据量大时才明显
- 所以阈值 8 是一个平衡点,不是随便选的
六、生产避坑 🟡
6.1 HashMap 死循环问题(JDK 7)
问题:多线程并发扩容时,链表节点顺序会反转,形成环形链表,导致 get() 死循环。
解决:JDK 8 通过改变扩容顺序(非递归、尾插)解决了这个问题。
6.2 不当的容量预设
七、总结
设计思想:
- 8 是一个保守的阈值(泊松分布概率极低)
- 6 而不是 8 是为了避免边界震荡
- 红黑树而非 AVL 是因为 HashMap 写多读少