源码解析 HashMap 的线程安全问题

    我们经常在面试中会遇到这个问题:

                  Q:HashMap是线程安全的吗?

                  A:不是

                  Q:为什么?

                  A:因为Hashmap 在多线程的情况下扩容会造成死循环的问题

上述回答并不准确, 因为在jdk1.7 和 jdk1.8 中的Hashmap是不一样的,只有在 jdk1.7 中才会出现上述问题,而在jdk1.8中是不会出现的。

现在我们通过源码看下为什么 jdk1.7 中Hashmap 在多线程的情况下扩容会造成死循环,我们直接看源码:

我们简单解释下, 在Hashmap放入元素的时候会有一个 addEntry 的方法, 里面会判断当 Hashmap 中的元素个数达到threshold 阈值的时候, 会进入resize 扩容方法,长度为原来的2倍。在 resize 中我们可以看到首先创建了一个 Entry 数组 newTable,长度为newCapacity,然后会进入 transfer 方法将原来数组中的元素转换到新数组 newTable中,现在我们着重看下transfer 方法:

方法不长,总共只有15行代码,但却是造成死循环的关键地方。 红框中的代码就是造成HashMap死循环的原因,现在我们假设原数组中的第一个元素为一个链表 2-->4,如下图所示:

    2--->4 

 

现在我们复现一下它是如何转换元素的,假设rehash 为 false,我们看下while 中的第一次遍历是什么样的:

上图可以看到e 为元素2,e.next 为元素 4,而 newTable 为新数组,所以 newTable[i] 是为null 的,while中第一次遍历结束后,我们看下新数组的元素存储情况:

   2--->null   

 

现在我们看下while 中的第二次遍历:

上图中可以看到 e 为元素4 ,而由于元素4和元素2是在原数组的同一个位置中的,所以他们的 e.hash 的值是一样的,这样算出来的下标 i 也是一样的,这样 newTable[i] 就是上一个元素 2。 所以 e.next=2,   newTable[i]=4 , 继续往下走,next 为null则循环结束。我们继续看下新数组中的元素存储情况:

   4--->2   

 

可以看到链表中的元素被反转了过来,这是因为jdk1.7 中HashMap 在扩容时转换元素的时候,若同一个位置有多个元素,则会采用头插法方式将元素放入新数组,也就是元素始终会在原来元素的头部插入。

我们假设现在有A,B2个线程,A 线程第一次循环结束后,在执行到592行的时候被挂起了。现在B线程进来执行完了2次循环,将4指向2,B结束,A继续执行,就出现2-->4, 4-->2的死循环情况。

我们继续看下jdk1.8 Hashmap 在多线程的情况下扩容的操作源码:

话不多说,直接看 resize 方法:

在查看resize方法时,突然觉得有可以优化的点,于是写了这篇resize优化,虽然推理过程不准确的,但整个思考过程比较完整,有兴趣的可以看看。

由于处于同一位置的哈希值大部分情况是不同的,而 e.hash & oldCap 只会有2种情况,等于0和不等于0,所以采用 loHead,loTail 和 hiHead,hiTail 这四个变量进行区分,而 loTail.next = null; 和  hiTail.next = null; 这2行的效果一样都是为了避免数据重复。

代码很长,我们着重看下722-726行 和 736-739行,还是2-->4 ,我们看下它2次的数据转换情况,第一次循环:

 

第一次循环loTail 为null, 所以 loHead 和 loTail 都等于元素 2 ,第二次循环:

可以看到在第二次循环完成后, 新数组中的 newTab[j] 中的元素还是原来的顺序 2-->4, 所以即使是在多线程的情况下扩容也不会发生死循环。但是jdk1.8中的HashMap依然不是线程安全的,是有可能发生数据覆盖的,我们还是看源码:

我们着重看下  630 行 和 631 行,在 630 行 和 631 行 可以看到 hash 与 n-1 按位与(也就是取余) 生成元素下标,然后取出该下标的元素判断是否为空,若为空则将元素放入tab数组中。

现在我们假设有A,B2个线程往HashMap中放入元素, A 线程带着 i=1,value=4 的元素执行到630行时被挂起了,而 B 线程带着i=1,value=5 的元素执行完成,结束。这时A线程继续执行 tab[1]=4 , 结束。这样value=5 的元素就被覆盖。

总结:

            Q:HashMap是线程安全的吗?

            A:不是

            Q:为什么?

            A:因为jdk1.7中,Hashmap 在多线程的情况下扩容由于链表的数据转换采用的是头插法所以会造成死循环的问题。而在jdk1.8中则进行了优化直接将原链表数据放到新数组中,不会造成死循环,但在多线程的情况下会导致数据覆盖

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值