一、学习指标

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...