hash冲突链表结构优化
hash冲突的链表长度超过阈值TREEIFY_THRESHOLD=8则转为红黑树结构
为什么阈值为8?
- 红黑树平均查找时间复杂度log(N),log(8)=3,链表平均查找长度为N/2,8/2=4,转换后性能更高
- 理想情况下,hash桶中节点的频率遵循泊松分布,桶长度超过8的概率非常小,约为6*10的负8次方,通常情况下不回发生结构转换
为什么不直接使用红黑树?
- 因为二叉树(红黑树其实是一种二叉树)虽然查询效率高,但是空间开销非常大,单个 TreeNode 需要占用的空间大约是普通 Node 的两倍,而在数据量很小时,红黑树与链表的查询效率不会差太多
- 前面阈值为8的原因中说到了,桶长度超过8的概率非常小,所以大概率情况下链表是更加的选择
resize优化
并发运输transfer
resize数据搬运运输过程并发执行,简而言之是搬运过程中数据被划分为N个区间,每个人负责一个区间,干完了就去下一个待处理区间继续干活。细节可以查看另一篇文章:https://blog.csdn.net/u010597819/article/details/96851257
- 搬运i节点
- 搬运完成后将该节点设置为ForwardingNode转发节点,之后i节点的请求都将转发至ForwardingNode节点,直至搬运完成(扩容完成)
rehash优化
将原有的rehash优化为掉,核心代码见下方,红黑树处代码逻辑相同
for (Node<K,V> p = f; p != lastRun; p = p.next) {
// ph节点hash值
int ph = p.hash; K pk = p.key; V pv = p.val;
// n老hash表长度table.length
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
// i老hash表中的下标索引位置
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
如果老hash值与老hash表长度按位与运算为0则放在原位置,否则放在原位置+老hash表长度的位置处,为什么这么写呢?扩容后原hash值的数据一定会落在i+n位置上吗?答案是肯定的,不然岂不是凉凉了
按照长度为8的表扩容至16代入运算看下
长度8二进制表示 :01000
长度16二进制表示:10000
条件判断:
- hash值与8按位与运算为0表示:hash值的二进制自右向左第4位一定为0
- hash值与8按位与运算不为0则表示:hash值的二进制自右向左第4位一定为1
取余数
- hash值对8取余数则是取hash值的二进制自右向左的3位二进制对应的十进制值
- hash值对16取余数则是取hash值的二进制自右向左的4位二进制对应的十进制值
因此满足条件2的情况下,hash值的后四位值也就确定了
- hash值后三位值为原hash表的索引值i,原因不赘述,详情可以查看:https://blog.csdn.net/u010597819/article/details/104881525
- hash值自右向左第4位一定为1,第四位为1,对应的十进制值即老hash表长度n=8
- 因此满足条件2的hash值对新表长度取余数则为 i+n
总结
- 为什么Map的大小必须是2的幂?我们的答案又多了一点
- 对于hash冲突的链表或红黑树的迁移不再需要rehash