前言
这个问题是扩容里面的一个遗留问题,前因后果大家可以去查阅上篇博客JUC1.8-ConcurrentHashMap源码学习-扩容方法解析transfer() ,在讲扩容机制时,篇幅过长,因此单独拿出来说明;
大家看过源码或者是读过笔者扩容那篇博客,就知道在每次扩容时,均在原容器的基础上,扩大2倍,贴下transfer()源码里面的位置:
if (nextTab == null) { // 开始初始化
try {
//n << 1 相当于2 * n 也就是2倍的n
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // 防止内存不够,出现OOM
sizeCtl = Integer.MAX_VALUE;
return;
}
//将生产好的2倍node数组,更新到成员变量
nextTable = nextTab;
//更新转移下标, n 就是老桶的长度
transferIndex = n;
}
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; 在代码也注释了,n代表原长度 左移1位,相当于2 * n, 不明白的可以查阅JUC1.8-ConcurrentHashMap源码学习-准备中的Java位运算部分。 so这篇我们就好好分析分析。
当笔者在遇到这种 莫名的关系时,我都会采用具体数字代入的方式,先找到有木有啥有规律的规则,然后 在联动该方法上下文进行分析,那么我就先带着大家用数据代入的方式看下:
总所周知的:在往容器put数据时,都得根据当前key值得hashcode取余n,然后找到对应的tab的下标位置,那么也可能发生hash碰撞后,就通过链表,或者红黑树将新的key放到旧的key后面。 例如:
现有key: “name”,“age”,“email”,“phone”,当前的哈希表容量是8
1. 通过java的hashCode() 函数可以计算出哈希码
2. 计算对应tab的下标 , (n-1) & hash
“name”字符串的哈希码是3373707(十进制),1100110111101010001011(二进制) & 00000111 = 0011,下标:3
“phone”字符串的哈希码是194811(十进制),101111100011110011(二进制)& 00000111 = 0011,下标:3
“age”字符串的哈希码是96511(十进制), 10111100011111111(二进制)& 00000111 = 0111,下标:7
“email”字符串的哈希码是96619420(十进制),101110000100100101110011100(二进制)& 00000111 = 0100,下标:4
所以目前对应的结构是:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
name | age | ||||||
phone |
3.此时来扩容,n << 1 新tab 长度就是16,
重新再来计算新的下标,这里就拿name和phone为例:
“name”字符串的哈希码是3373707(十进制),1100110111101010001011 & 00001111 = 1011,下标:11
“phone”字符串的哈希码是194811(十进制),101111100011110011 & 00001111 = 0011,下标:3
从这就可以看出来,
name的与7运算结果 是 0011 ,与15运算结果是1011 发现首位高一位,所以离开原有的节点, 那么新的位置 正好就是 原下标 + 原tab长度 = 3 + 8 = 11;
phone的与7运算结果 是 0011 ,与15运算结果是0011 发现首位没有变化,so所有并没变化下标值。
这就是扩大一倍的好处:
当初容量是8,8-1=7,7的二进制是111;
现在容量是16,16-1=15,15的二进制是1111因为他们都是全为1的形态,而扩大一倍后只是高位加了一个1,所以会留有大概一半的key在原来的空格,而有一半的key到相同的别的空格中,比如都是从3号位置到了11号位置中,这样也便于复制。通过位运算,代替了模运算!
总结
总结下:扩大2倍 = n<<1 也就相当于原长度的左移一位,也就是高位加了1, 那么在去做与运算时,在同一个链表的下面的key,如果高位也加1了,说明这个节点的对应的tab的下标也得跟着加上扩长的长度,俗称高位桶, 反正还是为0说明高位没有变化,俗称低位桶,放在原位置不动即可。 所以在扩容的时候,int runBit = fh & n; 用于来得到属于高位还是低位, 如果是低位下标不变,否者下标直接加就好了,就不用在重新计算新容器的取余计算了。