【死链】JDK1.7中HashMap在多线程环境的并发问题源码分析

一、HashMap在JDK1.7中的并发问题

在JDK1.7中的HashMap是以数组+链表组成的,我们先来看一下每次的put操作,可以分为以下三步:

  • 寻址
    • 1.根据key的hashcode()方法计算原始哈希值
    • 2.哈希值进一步和自身高16位做异或操作,得到更随机的hashcode
    • 3.hashcode再和数组长度-1做与操作,得到数组的下标
  • 判断是否扩容
    • 如果map中的数组元素个数超过了 数组长度 * 负载因子(0.75),则需要扩容
  • 比较并存放
    • 通过equals比较每个链表节点的值并赋值
      在这里插入图片描述

寻址和存放都不会出现并发问题,问题就出现在扩容部分,接下来我们详细展开看一下扩容部分的源码:

  • resize方法源码:
 void resize(int newCapacity) {
     Entry[] oldTable = table;
     int oldCapacity = oldTable.length;
     if (oldCapacity == MAXIMUM_CAPACITY) {
     threshold = Integer.MAX_VALUE;
     return;
     }
     // 创建2倍大小的新数组
     Entry[] newTable = new Entry[newCapacity];
     // 将旧数组的链表转移到新数组,就是这个方法导致的hashMap不安全,等下我们进去看一眼
     transfer(newTable, initHashSeedAsNeeded(newCapacity));
     table = newTable;
     // 重新计算扩容阈值(容量*加载因子)
     threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

出问题的就是这个transfer方法,其作用是把旧节点转移到扩容后的新map中,从1.7源码中可以看出它使用的是头插法来插入新节点,问题也正是出现在了这里

  1. transfer方法源码:
void transfer(Entry[] newTable, boolean rehash) {
     int newCapacity = newTable.length;
     // 遍历旧数组
     for (Entry<K,V> e : table) {
         // 遍历链表
         while(null != e) {
             //获取下一个元素,记录到一个临时变量,以便后面使用
             Entry<K,V> next = e.next;
             if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
             }
             // 计算节点在新数组中的下标
             int i = indexFor(e.hash, newCapacity);
             // 将旧节点插入到新节点的头部
             e.next = newTable[i];
             //这行才是真正把数据插入新数组中,前面那行代码只是设置当前节点的next
             //这两行代码决定了倒序插入
             //比如:以前同一个位置上是:3,7,后面可能变成了:7、3
             newTable[i] = e;
             //将下一个元素赋值给当前元素,以便遍历下一个元素
             e = next;  
         }  
     }
}

二、死链如何产生?

假设初始状态的HashMap数据如下图
在这里插入图片描述
假设此时再调用下一个put方法时,发生了扩容,会new一个容量翻倍的Hashmap,如果此时有两个线程,则会同时进入扩容方法
在这里插入图片描述
此时t2线程率先完成transfer方法,由于是头插法,所以元素会呈现倒序状态
在这里插入图片描述
此时线程t1开始进入transfer方法,每次用头插法赋值前,会先记录下旧map中该节点的next节点,以便进入下一次循环

  1. t1线程挂key:3的节点
    在这里插入图片描述
  2. t1线程挂key:7的节点,注意:在此间线程t2完成了所有新节点的赋值,所以此时7.next已经变成了3了
    在这里插入图片描述
  3. 由于节点不为null,会再次进入循环,此时再次挂key:3,next节点又为7,从而产生了死链
    在这里插入图片描述
    通过时序图可以更清楚的了解整个并发问题的产生
    在这里插入图片描述

三、如何解决HashMap并发问题

为了解决这个问题,JDK1.8把扩容的复制到新数组的算法从头插法改成了尾插法,并引入了红黑树。HashMap是非线程安全的,在多线程环境下还是推荐使用ConcurrentHashMap

参考文献

https://segmentfault.com/a/1190000024510131

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值