最近在复习准备面试,发现对于HashMap的1.7版本下头插法的问题,转载一篇优秀的文章
原文链接: https://blog.csdn.net/Mutou_ren/article/details/106222733
关于HashMap的线程安全问题,网上资料很多。对于1.7版本的闭环问题,看了很多网上的资料一直搞不懂,今天又琢磨了一下,终于明白了,用自己看得懂的方式记录下。
正常插入
首先说下闭环产生的原因:1.7的HashMap在扩容复制时,采用的是头插入法,这会导致原数组中的链表反转,即将原来的正向,复制成反向链表。
而在多线程环境下,可能存在其他线程完成了扩容复制操作,完成了后的数组链表变成了原来的反向链表。
由于可见性的保证,当前线程继续复制时,复制的时其他线程已经完成了的反向链表,但当前线程又会将其反转,导致又变成了正向链表,那么在当前线程复制过程中会既有正向的链表又有反向的链表,从而可能产生闭环的情况。
具体的图例如下
首先这里有一个前提,就是假设扩容后的key在hash之后的rehash值,对整个数组长度取模得到的value【即bucket的key哈希定位变与不变的情况下考虑的两种场景】
左边的为扩容前的分布,右边的为正常单线程情况下完成复制后的情况。
首先说下头插入的过程,首先遍历旧数组链表,拿到第一个元素A,保存其后续链表B,然后计算在新数组位置1并拿到首节点NULL,将A的next设置为NULL
1、将A放入新数组对应位置中
2、将原后续链表续到原数组中
以上是正常的移动过程,现在分析并发场景下产生的闭环问题和丢失问题
闭环问题
1、线程1执行到这里后挂起
2、线程2完成复制工作如下如
为什么索引1的位置是B->A,因为是头插法,线程2在index=1是先插A,再在头部插入B导致的
3、线程1唤醒,继续操作,由于可见性,老数组中的B的next实际上已经变成了A,线程1此时会将A至于新数组索引1中,即虚线框会发送覆盖
4、覆盖后变成下图
5、继续完成B的复制,如下图
6、旧数组中还剩下一个A,继续。还记得前面移动的步骤,首先会变更A的next指向B,然后放入新数组中。
7、最终会成为下图形成闭环
数据丢失
1、线程1在此处挂起
2、线程2完成复制,如下图
3、线程1苏醒,完成A的复制,此时B的next已经变成了NULL,C丢失
4、复制B,C没了
1.8尾插入法
由上面可知1.7的头插法带来的问题,数据丢失和闭环均是主要是由于改变链表方向导致的,设计者一开始使用头插入法是考虑碰撞时后加入的数据访问的可能性更大,头插入有更好的查找性能。
1.8改为尾插入,不会进行链表方向的改变,因此不会发生上面两种情况