共计 3254 个字符,预计需要花费 9 分钟才能阅读完成。
一、学习指标
1、HashMap 线程不平安起因:
起因:
- JDK1.7 中,因为多线程对 HashMap 进行扩容,调用了 HashMap#transfer(),具体起因:某个线程执行过程中,被挂起,其余线程曾经实现数据迁徙,等 CPU 资源开释后被挂起的线程从新执行之前的逻辑,数据曾经被扭转,造成死循环、数据失落。
- JDK1.8 中,因为多线程对 HashMap 进行 put 操作,调用了 HashMap#putVal(),具体起因:假如两个线程 A、B 都在进行 put 操作,并且 hash 函数计算出的插入下标是雷同的,当线程 A 执行完第六行代码后因为工夫片耗尽导致被挂起,而线程 B 失去工夫片后在该下标处插入了元素,实现了失常的插入,而后线程 A 取得工夫片,因为之前曾经进行了 hash 碰撞的判断,所有此时不会再进行判断,而是间接进行插入,这就导致了线程 B 插入的数据被线程 A 笼罩了,从而线程不平安。
改善:
- 数据失落、死循环曾经在在 JDK1.8 中曾经失去了很好的解决,如果你去浏览 1.8 的源码会发现找不到 HashMap#transfer(),因为 JDK1.8 间接在 HashMap#resize() 中实现了数据迁徙。
2、HashMap 线程不平安的体现:
- JDK1.7 HashMap 线程不安整体当初:死循环、数据失落
- JDK1.8 HashMap 线程不安整体当初:数据笼罩
二、HashMap 线程不平安、死循环、数据失落、数据笼罩的起因
1、JDK1.7 扩容引发的线程不平安
HashMap 的线程不平安次要是产生在扩容函数中,其中调用了 JDK1.7 HshMap#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];
newTable[i] = e;
e = next;
}
}
}
复制代码
这段代码是 HashMap 的扩容操作,从新定位每个桶的下标,并采纳头插法将元素迁徙到新数组中。头插法会将链表的程序翻转,这也是造成死循环的关键点。了解了头插法后再持续往下看是如何造成死循环以及数据失落的。
2、扩容造成死循环和数据失落
假如当初有两个线程 A、B 同时对上面这个 HashMap 进行扩容操作:
失常扩容后的后果是上面这样的:
然而当线程 A 执行到下面 transfer 函数的第 11 行代码时,CPU 工夫片耗尽,线程 A 被挂起。即如下图中地位所示:
此时线程 A 中:e=3、next=7、e.next=null
当线程 A 的工夫片耗尽后,CPU 开始执行线程 B,并在线程 B 中胜利的实现了数据迁徙
重点来了,依据 Java 内存模式可知,线程 B 执行完数据迁徙后,此时主内存中 newTable 和 table 都是最新的,也就是说:7.next=3、3.next=null。
随后线程 A 取得 CPU 工夫片继续执行 newTable[i] = e,将 3 放入新数组对应的地位,执行完此轮循环后线程 A 的状况如下:
接着继续执行下一轮循环,此时 e =7,从主内存中读取 e.next 时发现主内存中 7.next=3,此时 next=3,并将 7 采纳头插法的形式放入新数组中,并继续执行完此轮循环,后果如下:
此时没任何问题。
上轮 next=3,e=3,执行下一次循环能够发现,3.next=null,所以此轮循环将会是最初一轮循环。
接下来当执行完 e.next=newTable[i] 即 3.next= 7 后,3 和 7 之间就相互连接了,当执行完 newTable[i]= e 后,3 被头插法从新插入到链表中,执行后果如下图所示:
下面说了此时 e.next=null 即 next=null,当执行完 e =null 后,将不会进行下一轮循环。到此线程 A、B 的扩容操作实现,很显著当线程 A 执行完后,HashMap 中呈现了环形构造,当在当前对该 HashMap 进行操作时会呈现死循环。
并且从上图能够发现,元素 5 在扩容期间被莫名的失落了,这就产生了数据失落的问题。
3、JDK1.8 中的线程不平安
下面的扩容造成的数据失落、死循环曾经在在 JDK1.8 中曾经失去了很好的解决,如果你去浏览 1.8 的源码会发现找不到 HashMap#transfer(),因为 JDK1.8 间接在 HashMap#resize() 中实现了数据迁徙。
为什么说 JDK1.8 会呈现数据笼罩的状况? 咱们来看一下上面这段 JDK1.8 中的 put 操作代码:
其中第六行代码是判断是否呈现 hash 碰撞,假如两个线程 A、B 都在进行 put 操作,并且 hash 函数计算出的插入下标是雷同的,当线程 A 执行完第六行代码后因为工夫片耗尽导致被挂起,而线程 B 失去工夫片后在该下标处插入了元素,实现了失常的插入,而后线程 A 取得工夫片,因为之前曾经进行了 hash 碰撞的判断,所有此时不会再进行判断,而是间接进行插入,这就导致了线程 B 插入的数据被线程 A 笼罩了,从而线程不平安。
除此之前,还有就是代码的第 38 行处有个 ++size,咱们这样想,还是线程 A、B,这两个线程同时进行 put 操作时,假如以后 HashMap 的 zise 大小为 10,当线程 A 执行到第 38 行代码时,从主内存中取得 size 的值为 10 后筹备进行 + 1 操作,然而因为工夫片耗尽只好让出 CPU,线程 B 高兴的拿到 CPU 还是从主内存中拿到 size 的值 10 进行 + 1 操作,实现了 put 操作并将 size=11 写回主内存,而后线程 A 再次拿到 CPU 并继续执行 (此时 size 的值仍为 10),当执行完 put 操作后,还是将 size=11 写回内存,此时,线程 A、B 都执行了一次 put 操作,然而 size 的值只减少了 1,所有说还是因为数据笼罩又导致了线程不平安。
三、如何使 HashMap 在多线程状况下进行线程平安操作?
应用 Collections.synchronizedMap(map),包装成同步 Map,原理就是在 HashMap 的所有办法上 synchronized。
例如:Collections.SynchronizedMap#get()
public V get(Object key) {synchronized (mutex) {return m.get(key);
}
}
复制代码
四、总结
1、HashMap 线程不平安起因:
起因:
- JDK1.7 中,因为多线程对 HashMap 进行扩容,调用了 HashMap#transfer(),具体起因:某个线程执行过程中,被挂起,其余线程曾经实现数据迁徙,等 CPU 资源开释后被挂起的线程从新执行之前的逻辑,数据曾经被扭转,造成死循环、数据失落。
- JDK1.8 中,因为多线程对 HashMap 进行 put 操作,调用了 HashMap#putVal(),具体起因:假如两个线程 A、B 都在进行 put 操作,并且 hash 函数计算出的插入下标是雷同的,当线程 A 执行完第六行代码后因为工夫片耗尽导致被挂起,而线程 B 失去工夫片后在该下标处插入了元素,实现了失常的插入,而后线程 A 取得工夫片,因为之前曾经进行了 hash 碰撞的判断,所有此时不会再进行判断,而是间接进行插入,这就导致了线程 B 插入的数据被线程 A 笼罩了,从而线程不平安。
改善:
- 数据失落、死循环曾经在在 JDK1.8 中曾经失去了很好的解决,如果你去浏览 1.8 的源码会发现找不到 HashMap#transfer(),因为 JDK1.8 间接在 HashMap#resize() 中实现了数据迁徙。
2、HashMap 线程不平安的体现:
- JDK1.7 HashMap 线程不安整体当初:死循环、数据失落
- JDK1.8 HashMap 线程不安整体当初:数据笼罩
《2020 最新 Java 根底精讲视频教程和学习路线!》
链接:https://juejin.cn/post/691752…