多线程情况下HashMap的头插法和尾插法,为什么尾插法会避免死循环?
我觉得尾插法也会出现环形链表的问题,为什么网上直说头插法存在这样的问题,多线程情况下就是会出现指针指向问题,网上大多数都讲的不是很清楚。
1、数据插入原理
在分析原因之前,我先带大家了解一下JDK1.7中HashMap插入数据的原理,来看动画演示:
由于JDK 1.7中HashMap的底层存储结构采用的是数组 加 链表的方式。
而HashMap在数据插入时又采用的是头插法,也就是说新插入的数据会从链表的头节点进行插入。
因此,HashMap正常情况下的扩容就是是这样一个过程。我们来看,旧HashMap的节点会依次转移到新的HashMap中,旧HashMap转移链表元素的顺序是A、B、C,而新HashMap使用的是头插法插入,所以,扩容完成后最终在新HashMap中链表元素的顺序是C、B、A
2、导致死循环的原因
接下来,我通过动画演示的方式,带大家彻底理解造成HashMap死循环的原因。我们按以下三个步骤来还原并发场景下HashMap扩容导致的死循环问题:
第一步:线程启动,有线程T1和线程T2都准备对HashMap进行扩容操作, 此时T1和T2指向的都是链表的头节点A,而T1和T2的下一个节点分别是T1.next和T2.next,它们都指向B节点。
第二步:开始扩容,这时候,假设线程T2的时间片用完,进入了休眠状态,而线程T1开始执行扩容操作,一直到线程T1扩容完成后,线程T2才被唤醒。
T1完成扩容之后的场景就变成动画所示的这样。
因为HashMap扩容采用的是头插法,线程T1执行之后,链表中的节点顺序发生了改变。但线程T2对于发生的一切还是不可知的,所以它指向的节点引用依然没变。如图所示,T2指向的是A节点,T2.next指向的是B节点。
当线程T1执行完成之后,线程T2恢复执行时,死循环就发生了。
因为T1执行完扩容之后,B节点的下一个节点是A,而T2线程指向的首节点是A,第二个节点是B,这个顺序刚好和T1扩容之前的节点顺序是相反的。T1执行完之后的顺序是B到A,而T2的顺序是A到B,这样A节点和B节点就形成了死循环。
3、解决方案
避免HashMap发生死循环的常用解决方案有三个:
1)、使用线程安全的ConcurrentHashMap替代HashMap,个人推荐使用此方案。
2)、使用线程安全的容器Hashtable替代,但它性能较低,不建议使用。
3)、使用synchronized或Lock加锁之后,再进行操作,相当于多线程排队执行,也会影响性能,不建议使用。
4、总结
HashMap死循环只发生在JDK1.7版本中,主要原因是JDK1.7中的HashMap,在头插法 加 链表 加 多线程并发 加 扩容这几个情形累加到一起就会形成死循环。多线程环境下建议采用ConcurrentHashMap替代。在JDK1.8中,HashMap改成了尾插法,解决了链表死循环的问题。