目录
HashMap 将链表转化为红黑树的阈值设定为 8 的背后原因,与一个重要的统计学原理——泊松分布密切相关。该原理阐明了在单位时间(或面积、体积)内,随机事件的平均发生次数遵循泊松分布,其概率方程如下所示::
在哈希表中,我们将哈希桶作为单位面积,并将插入操作视为一系列随机事件,其中每个事件代表将一个 Key 映射到哈希桶内。因此,符号 λ \lambda λ 表示每个桶内平均存储元素的数量,而 P ( X = k ) P(X = k) P(X=k) 表示有 k k k 个 Key 被映射到同一个哈希桶的概率。
根据 HashMap 源码注释的信息,当 λ = 0.5 \lambda = 0.5 λ=0.5 时,以下是 k k k 从 0 到 8 对应的概率值:
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
根据前述内容,可以得出在 λ = 0.5 \lambda = 0.5 λ=0.5 的情况下,哈希桶内哈希冲突元素数量 k = 8 k = 8 k=8 的概率极低。因此,HashMap 设定只有当链表长度超过 8 时,才会考虑将其转换为红黑树来处理,以在平衡查询效率和内存占用方面取得良好的性能平衡。
然而,还有一个重要问题需要解答:我们提到了 λ \lambda λ 被设定为 0.5,而泊松分布概率方程的准确性与频率 λ \lambda λ 的选择紧密相关。那么,Java是如何确定 λ = 0.5 \lambda = 0.5 λ=0.5 的呢?答案涉及到HashMap的扩容因子为0.75。
HashMap 扩容因子:
为了在容量和性能之间实现平衡,HashMap 将加载因子设置为 0.75。这一设定旨在维持合适的容器大小,以兼顾性能和空间的最佳折中。
首先,我们从一个理想情况出发:我们假设哈希算法能够完美地分散数据,因此在向 HashMap 中插入数据时,不会发生任何哈希冲突。然后,随着数据的连续插入,HashMap 会多次触发扩容操作。由于扩容因子设定为0.75,每次扩容前哈希表内的数据量占容器的比例为0.75,而每次扩容后,该比例为0.375。因此,在数据持续添加的过程中,哈希表内数据量的比例会在0.375和0.75之间呈锯齿状波动:
^
|
| _______ 0.75
| /| /| /| _____________0.5625
|/ |/ |/ |/ _______0.375
+--------------------------------->
在忽略方差的情况下,哈希表容量占比的期望值约为 0.5625,也就是说,平均每个桶内有 0.5 个元素,这便是源码中 λ \lambda λ 值的由来。
参考资料:Hashmap 底层原理