源码解析 HashMap 的线程安全问题,mysql 索引左前缀原理
Q:为什么?
A:因为 Hashmap 在多线程的情况下扩容会造成死循环的问题
上述回答并不准确, 因为在 jdk1.7 和 jdk1.8 中的 Hashmap 是不一样的,只有在 jdk1.7 中才会出现上述问题,而在 jdk1.8 中是不会出现的。
现在我们通过源码看下为什么?jdk1.7 中 Hashmap 在多线程的情况下扩容会造成死循环,我们直接看源码:
![](https://img-blog.csdnimg.cn/2
0191121162703642.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1JveWFsX2xy,size_16,color_FFFFFF,t_70)
我们简单解释下, 在 Hashmap 放入元素的时候会有一个 addEntry 的方法, 里面会判断当 Hashmap 中的元素个数达到 threshold 阈值的时候, 会进入 resize 扩容方法,长度为原来的 2 倍。在 resize 中我们可以看到首先创建了一个 Entry 数组 newTable,长度为 newCapacity,然后会进入 transfer 方法将原来数组中的元素转换到新数组?newTable 中,现在我们着重看下 transfer 方法:
方法不长,总共只有 15 行代码,但却是造成死循环的关键地方。 红框中的代码就是造成 HashMap 死循环的原因,现在我们假设原数组中的第一个元素为一个链表 2-->4,如下图所示:
<table border="1" cellpadding="1" cellspacing="1" style="width:147px;"><tbody><tr><td style="width:79px;"> 2--->4</td><td style="width:67px;"> </td></tr></tbody></table>
现在我们复现一下它是如何转换元素的,假设 rehash 为 false,我们看下 while 中的第一次遍历是什么样的:
上图可以看到 e 为元素 2,e.next 为元素 4,而 newTable 为新数组,所以 newTable[i] 是为 null 的,while 中第一次遍历结束后,我们看下新数组的元素存储情况:
<table border="1" cellpadding="1" cellspacing="1" style="width:236px;"><tbody><tr><td style="width:89px;"> 2--->null</td><td style="width:46px;"> </td><td style="width:54px;"> </td><td style="width:44px;"> </td></tr></tbody></table>
现在我们看下 while 中的第二次遍历:
上图中可以看到 e 为元素 4 ,而由于元素 4 和元素 2 是在原数组的同一个位置中的,所以他们的 e.hash 的值是一样的,这样算出来的下标 **i 也是一样的,**这样 newTable[i] 就是上一个元素 2。 所以 e.next=2,? ?newTable[i]=4 , 继续往下走,next 为 null 则循环结束。我们继续看下新数组中的元素存储情况:
<table border="1" cellpadding="1" cellspacing="1" style="width:236px;"><tbody><tr><td style="width:89px;"> 4--->2</td><td style="width:46px;"> </td><td style="width:54px;"> </td><td style="width:44px;"> </td></tr></tbody></table>
可以看到链表中的元素被反转了过来,这是因为 jdk1.7 中 HashMap 在扩容时转换元素的时候,若同一个位置有多个元素,则会采用头插法方式将元素放入新数组,也就是元素始终会在原来元素的头部插入。
我们假设现在有 A,B2 个线程,A 线程第一次循环结束后,在执行到 592 行的时候被挂起了。现在 B 线程进来执行完了 2 次循环,将 4 指向 2,B 结束,A 继续执行,就出现 2-->4, 4-->2 的死循环情况。
我们继续看下 jdk1.8?Hashmap 在多线程的情况下扩容的操作源码:
话不多说,直接看 resize 方法:
评论