此博客将集中解决以下问题:
为什么HashMap不应该在多线程环境中使用?
他能导致无限循环吗?
当get方法在HashMap中进入无限循环时?
如果在多线程环境中使用HashMap,则Get和Put操作可能引导你进入无限循环
什么是rehashing?
HashMap的默认容量为16,加载因子为0.75,这意味着当第13个 键值对在地图中输入时,HashMap将使其容量加倍(16 * 0.75 = 12)。
直到第12个键值对,Hashmap将继续将项目放在地图中,一旦您尝试放置第13个键值对,重新开始进程就会重新开始。
加载因子: 加载因子是一种度量“直到什么加载,hashmap可以允许元素在其大小增加之前放入其中。”Rehasing反转节点的排序
在Rehashing过程中,
Hashmap首先创建一个双倍大小的新数组(Buckets)。
Hashmap 将旧存储桶中的键值对传输到新存储桶。
键值对将在新存储桶中反转,因为Hashmap将在新存储桶的开头添加键值对,而不是在结尾处添加键值对。
Hashmap在开始时添加新的键值对,以避免每次遍历链表并保持不变的性能。
让我们看看转移过程,例如,
当2个线程尝试将第13个键值对放入Hashmap时会发生什么?
当2个线程同时尝试访问HashMap时,您可能会遇到无限循环。
让我们看看它是如何发生的,
为了清楚起见,让我们将2个线程命名为线程1和线程2,并且两者都尝试放置第13个键值对。
很明显,在放入第13个键值对之前,Hashmap必须首先执行Rehashing过程,因为第13个键值对超过了加载因子限制。
此外,这里的Hashmap由Thread1和Thread2访问,因此无法保证哪个Thread将首先获得访问权限。
我们假设两个线程都到达一个地方,在那里它们都识别出载荷因子限制已经越过并且地图需要重新划分。这是两个线程将尝试调用以下方法来将旧桶中的键值对传输到新桶的地方。
调用方法transfer()以将键值对从
旧桶传输到新桶
void transfer(Entry[] newTable) {<font></font>
Entry[] src = table;<font></font>
int newCapacity = newTable.length;<font></font>
for (int j = 0; j < src.length; j++) {<font></font>
Entry<k> e = src[j];<font></font>
if (e != null) {<font></font>
src[j] = null;<font></font>
<font></font>
// --------- 下面的代码会产生问题 --------- <font></font>
do {<font></font>
<font></font>
Entry<k> next = e.next; <font></font>
int i = indexFor(e.hash, newCapacity);<font></font>
e.next = newTable[i];<font></font>
newTable[i] = e;<font></font>
e = next;<font></font>
<font></font>
} while (e != null);<font></font>
// --------- 直到这里 --------- <font></font>
<font></font>
}<font></font>
}<font></font>
}<font></font>
让我们看看Hashmap如何以无限循环结束。
下面你将看到线程1和线程2步骤,以便快速浏览。
注意:如果您在理解Thread步骤时遇到困难,可以转到后面的部分,其中详细解释了transfer()方法和Thread Steps。
线程1有机会执行。
执行第一行内部循环(在transfer()方法中)后,线程1指向第一个键值对和下一个(第二个)键值对以开始传输过程。
在执行任何步骤之前,线程1松开控件,线程2有机会执行。
因此,线程1的当前状态是, e =节点90,下一个=节点1。
线程2有机会执行。
1.幸运的是,线程2执行完整的transfer()方法而不会失去对其他线程的控制。
2.在将旧桶中的键值对传输到新存储桶时,键值对将在新存储桶中反转,因为hashmap将在开始时而不是在结尾处添加键值对。这样做是为了避免每次遍历链表并保持不变的性能。
-
3.线程2将所有键值对从旧存储桶传输到新存储桶,线程1将有机会执行。
线程2执行后,Hashmap状态如下图所示,
线程1再次有机会执行。
现在线程1将恢复transfer()过程,但它将以Infinte循环结束节点。
这发生因为线程2实际上反转了节点链接。
任何进一步的get / put请求都将以无限循环结束。
还不清楚它是如何在无限循环中结束的?
让我们逐步了解详细的逐步算法中两个线程的每个步骤。
在进入细节之前让我们了解transfer()方法的作用:
第1 步: 输入<k> next = e.next;
第2 步: int i = indexFor(e.hash,newCapacity);
第3 步: e.next = newTable [i];
第4 步: newTable [i] = e;
注意:
第5步: e =下一个;
太多的代码....现在让我们从竞争条件开始....
线程1步骤
线程1有机会执行,并执行以下步骤,
1.线程1尝试放入第13个键值对,
2.线程1发现达到阈值限制并且它创建了容量增加的新桶。因此地图的容量从16增加到32。
3.线程1现在开始传输过程,用于将存储在桶0
的存储键值从旧数组传输到存储桶0处的新数组(假设为在新数组中存储键值对而评估的索引与索引0相同)。
对于传输,它调用transfer()方法并进入循环。
- 4.执行第一行内部循环(在transfer()方法中)后,线程1指向第一个键值对和下一个(第二个)键值对以开始传输过程。
在执行任何步骤之前,线程1松开控件,线程2有机会执行。
-
5.因此,线程1的当前状态是, e =节点90,下一个=节点1。
线程1指向键值对之后,在开始传输过程之前,松开控件,线程2有机会执行。
线程2步骤
线程2有机会执行,并执行以下步骤,
1.线程2尝试放入第13个键值对,
2.线程2发现达到阈值限制并且它创建了容量增加的新桶。因此地图的容量从16增加到32。
-
3.线程2现在开始传输过程,用于将存在于存储桶0
的存储键值从旧数组传送到存储桶0处的新数组(假设为在新数组中存储键值对而评估的索引与索引0相同)。对于传输,它调用transfer()方法并进入循环。
4.线程2指向第一个键值对和下一个(第二个)键值对以开始传输过程。
5.幸运的是,线程2执行完整的transfer()方法而不会失去对其他线程的控制。
6.在将旧桶中的键值对传输到新存储桶时,键值对将在新存储桶中反转,因为hashmap将在开始时而不是在结尾处添加键值对。这样做是为了避免每次遍历链表并保持不变的性能。
7.线程2将所有键值对从旧存储桶传输到新存储桶,线程1将有机会执行。
线程2执行后,Hashmap状态如下图所示
这里就没有吧线程1和线程2的详细执行步骤的图罗列出来。可根据自我理解情况查看源代码
如有错误,欢迎留言联系作者