jdk7和8中关于HashMap和concurrentHashMap的扩容过程总结,以及HashMap死循环

题外话:为什么要hashcode进行spread? 充分使用key.hashCode()的高16位信息,保证hash分布更分散,

扩容操作是新建2倍于原表大小的新表,并将原表结点拷贝一份放在新表中,对原表无修改或修改很小。当原表所有结点都已被拷贝到新表中后,原表会被垃圾回收。

在jdk7中的HashMap实现类中,数组+链表。扩容操作是将原数组的结点一一进行hash计算,然后一一挂接到新数组上,所以不是基于复制结点的机制。
在jdk7中的ConcurrentHashMap实现类中,段(segment)+数组+链表。扩容操作是先遍历数组元素,在每个数组元素上遍历一遍链表,找到链表的最后n个结点(这n个结点在新数组一定属于同一个数组位置上),把这n个结点先挂接到新数组的数组位置上,这也叫lastRun机制。至于原数组的头结点到倒数n个结点之间的结点,再遍历一遍,通过复制每个结点的机制挂接到新数组上。

jdk8中的HashMap实现类中,数组+链表/红黑树。扩容操作是将原数组每个结点的hash值和原数组长度进行“与”操作,结果等于0代表该结点位置不变,落在新数组的同样位置,否则该结点在新数组的位置是[j + oldCap]上。
原因是:扩容是将数组长度扩大一倍,假如原长度是16(二进制是10000,掩码1111),新长度是32(二进制是100000,掩码11111),那么在计算结点所落的位置时,hash值原本是低4位参与计算,扩容后变成hash值低5位参与计算,这样的话,当参与运算的最高位也就是第五位,是1时,必然落在扩容后的新的位置,是0时,必然位置不变,因为原数组长度16的二进制第五位是1,所以通过将结点的hash值和原数组长度进行与操作,就可以知道结点在新数组中是保持相对不变还是落在高一点的位置上。

jdk8中的ConcurrentHashMap实现类中,数组+链表/红黑树。扩容操作是多个线程参与共同完成的,相比于jdk7版本的扩容,jdk8的扩容属于渐进式扩容,不是一蹴而就。将原数组长度为n作为扩容任务的总数,切分成m块作为m个小任务,每个小任务有且只有一个线程来负责完成扩容(因为扩容后的数组长度是原来的2倍,结点要么在新数组的相对原位置i,要么在i+OldTableSize处,所以其他线程在扩容别的小任务时,不会和当前线程存在位置冲突)。对于扩容时,链表同样先找lastRun然后挂接到新数组上,前面的结点再通过复制的机制挂接到新数组上。

HashMap并发问题:死循环的原因
Hashmap的Resize包含扩容和ReHash两个步骤,ReHash在并发的情况下可能会形成链表环。因为,链表采用头插法,将原数组转移到新数组时,会从前向后遍历链表结点,头插法机制恰好使新数组中结点的相对顺序和原数组中颠倒过来。在并发的时候,假如原来的结点顺序被线程A颠倒了,而被挂起的线程b在恢复执行后,拿扩容前的节点和顺序继续完成第一次循环,而后又遵循A线程扩容后的链表顺序重新排列链表中的顺序,即又颠倒了一下顺序,最终形成了环。

原文地址:https://www.cnblogs.com/cnblogszs/p/10426916.html