1. transfer(jdk1.7)

  1. void transfer(Entry[] newTable, boolean rehash) {
  2. //获取新哈希表的容量
  3. int newCapacity = newTable.length;
  4. //循环原哈希表
  5. for (Entry<K,V> e : table) {
  6. //循环原Entry线性链表
  7. while(null != e) {
  8. Entry<K,V> next = e.next;
  9. //根据是否启用rehash判断是否为每一个key生成新的哈希值
  10. //如果当前entry的key等于null,则重新设置当前entry的哈希值为0
  11. //如果不为null,则对当前entyr的哈希值根据哈希干扰因子(HashSeed)进行重
  12. //新计算赋值
  13. if (rehash) {
  14. e.hash = null == e.key ? 0 : hash(e.key);
  15. }
  16. //根据新的哈希值和新的容量计算该entry应该存放的数组下标位置
  17. int i = indexFor(e.hash, newCapacity);
  18. e.next = newTable[i];
  19. newTable[i] = e;
  20. e = next;
  21. }
  22. }
  23. }

我们对以上的语句使用一个示例进行解读:1.分析图解:
1:为了方便计算,假设hash算法为key mod链表长度;
2:初始时数组长度2,key = 3, 7, 5 ,初始在表table[1]节点;3:然后resize后,hash数组长度为4

2.第一次while循环

  1. void transfer(Entry[] newTable, boolean rehash) {
  2. int newCapacity = newTable.length; //newCapacity = 4;
  3. for (Entry<K,V> e : table) { //第一次for循环数组下标为0 ; e == null 跳出while循环 ;第二次for循环数组下标为1;e = 3;
  4. while(null != e) { //第一次while循环 e = 3;e != null
  5. Entry<K,V> next = e.next;// next = e.next = 7;
  6. if (rehash) { //暂时忽略rehash
  7. e.hash = null == e.key ? 0 : hash(e.key);
  8. }
  9. int i = indexFor(e.hash, newCapacity); //新数组下标 i = (3 % 4) = 3;
  10. e.next = newTable[i]; //e.next = newTable[i] = newTable[3] = null;
  11. newTable[i] = e; // newTable[3] = 3;
  12. e = next; //e = next = 7;
  13. }
  14. }
  15. }

image.png
3.第二次while循环

  1. void transfer(Entry[] newTable, boolean rehash) {
  2. int newCapacity = newTable.length; //newCapacity = 4;
  3. for (Entry<K,V> e : table) { //第一次for循环数组下标为0 ; e == null 跳出while循环 ;第二次for循环数组下标为1;e = 3;
  4. while(null != e) { // 第二次while 循环 e = 7; e != null;
  5. Entry<K,V> next = e.next;// next = e.next = 5;
  6. if (rehash) { //暂时忽略rehash
  7. e.hash = null == e.key ? 0 : hash(e.key);
  8. }
  9. int i = indexFor(e.hash, newCapacity); //新数组下标 i = (7 % 4) = 3;
  10. //e.next = newTable[i] = newTable[3] = 3;
  11. //这里要清楚newTable[3]的值已经是新数组中的3.
  12. //因此这一步的操作实际上是把当前的e(7)的next指针指向了3. -也就是常说的头部插入法
  13. e.next = newTable[i];
  14. newTable[i] = e; //newTable[3] = 7;
  15. e = next; //e = next = 5; 后面while循环及for循环以此类推
  16. }
  17. }
  18. }

image.png

HashMap的转存使用头部插入法。
分析图解:
    1:为了方便计算,假设hash算法为key mod链表长度;
    2:初始时数组长度2,key = 3, 7, 5 初始在表table[1]节点;
    3:然后resize后,hash数组长度为4
image.png
如果不发生异常,正常结果为:
image.png

JDK1.7-Hashmap扩容死锁问题
JDK1.7—>-两个线程同时并发的对原数组扩容。由于链表都使用头插法,两个线程在用指针指向后,会形成循环链表。 然后再新数据进入的时候,会先从链表上找是否存在对应的key。然后在循环链表中一直死循环,如法插入。

1:假设线程A在某处挂起

  1. void transfer(Entry[] newTable, boolean rehash) {
  2. int newCapacity = newTable.length;
  3. for (Entry<K,V> e : table) {
  4. while(null != e) {
  5. Entry<K,V> next = e.next;
  6. if (rehash) {
  7. e.hash = null == e.key ? 0 : hash(e.key);
  8. }
  9. int i = indexFor(e.hash, newCapacity);
  10. e.next = newTable[i];
  11. //线程A在此处挂起
  12. newTable[i] = e;
  13. e = next;
  14. }
  15. }
  16. }

image.png
当A挂起后,线程B正常执行完
image.png
由于线程B已经执行完毕,根据Java内存模型,现在newTable和table中的Entry都是主存中最新值:7.next=3,3.next=null。
此时切换回线程A上,在线程A挂起时继续执行
newTable[i]=e ——> newTable[3]=3
e=next ——> e=7

继续下一次循环,e=7

next=e.next ——> next=3【从主存中取值】
e.next=newTable[3] ——> e.next=3【从主存中取值】
newTable[3]=e ——> newTable[3]=7
e=next ——> e=3

e不为空继续下一次循环 e=3
next=e.next ——> next=null
e.next=newTable[3] ——> e.next=7 即:3.next=7
newTable[3]=e ——> newTable[3]=3
e=next ——> e=null

此次循环后3.next = 7 但上一步 7.next =3 行成环行链表
image.png
在后续操作中只要涉及轮询hashmap的数据结构,就会在这里发生死循环

JDK1.7-HashMap扩容死锁问题复现

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. import java.util.concurrent.atomic.AtomicInteger;
  4. /**
  5. * JDK1.7 HashMap死锁问题复现
  6. */
  7. public class TestHashMapThread implements Runnable{
  8. //为了尽快扩容,设置初始大小为2
  9. private static Map<Integer,Integer> map = new HashMap<>(2);
  10. private static AtomicInteger atomicInteger = new AtomicInteger();
  11. @Override
  12. public void run() {
  13. while (atomicInteger.get() < 1000000){
  14. map.put(atomicInteger.get(),atomicInteger.get());//往线程中插入值
  15. atomicInteger.incrementAndGet();//递增
  16. }
  17. }
  18. public static void main(String[] args){
  19. for(int i = 0; i < 10 ; i++){
  20. //启动十个线程去做处理
  21. new Thread(new TestHashMapThread()).start();
  22. }
  23. }
  24. }

2. JDK1.7-HashMap扩容数据丢失问题

其次,1.7中扩容还会出现数据丢失
模拟另外一种情况
image.png
同样线程A在固定位置挂起
image.png
线程B完成扩容
image.png

同样注意由于线程B执行完成,newTable和table都为最新值:5.next=null。
此时切换到线程A,在线程A挂起时:e=7,next=5,newTable[3]=null。
执行newtable[i]=e,就将7放在了table[3]的位置,此时next=5。接着进行下一次循环:
e=5
next=e.next ——> next=null,从主存中取值
e.next=newTable[1] ——> e.next=5,从主存中取值
newTable[1]=e ——> newTable[1]=5
e=next ——> e=null
将5放置在table[1]位置,此时e=null循环结束,3元素丢失,并形成环形链表。并在后续操作hashmap时造成死循环。