@扩容形成环形链表死循环问题分析
场景
我们知道jdk1.7 hashmap的底层数据结构为hashtable加链表构成的,put操作时会是头插入法,并且还涉及扩容,单线程下这个是没有问题,现在场景出在多线程时,那么这个扩容resize是会出现问题的,咱们现在针对这个问题去查看扩容的源码来分析一下为什么会出现这个问题?
源码分析
这里就是resize方法实际上扩容和entry的转移是在transfer方法里面进行,下面我们直接进入transfer方法
现在假设有2个线程,即线程t1和线程t2执行这个扩容方法,我们针对这一场景进行源码分析,为容易理解配以图来表示。
1.假设扩容前的hashmap为如图所示,hashtable长度为2,此时有一个Entry节点{k3,v3}挂在hashtable的槽位1上面
- 假如此时有2个线程t1,t2同时对该hashmap进行put操作,假设线程t1put {k1,v1},线程t2put{k2,v2}, 线程t1在put完后该hashtable需要进行扩容,假设线程t1执行到扩容transfer方法的代码1处,此时线程2抢到cpu的执行时间,线程t1挂住在此,此时的hashmap的数据如图所示
此时t2继续执行扩容代码逻辑,t2扩容完毕后假设{k1,v1}和{k2,v2}还在同一链表上,那么t2扩容的hashmap数据如图所示
- 此时t1继续执行代码1下的执行逻辑,进行扩容,此时执行到代码e.next = newTable[i]; 即e.next=null,所以{k1,v1}的next执行新的hashtable, newTable[i] = e,所以线程t1的新hashtable链表的第一个元素指向线程t2的{k1,v1}, 最后一行e=next ,所以e执行{k2,v2}
继续下一次循环,e.next=newTable[i],所以{k2,v2}的next指向{k1,v1},newTable[i] = e,所以线程t1的链表第一个元素就变成{k2,v2},再执行到e=next,即e指向{k1,v1}
继续下一次循环,,e.next=newTable[i],所以{k1,v1}的next指向{k2,v2},e.next =null, e=null,循环结束,到此环形链表已形成,如图下图所示。后续的put操作和get操作一旦涉及到在此槽位的环形链表上进行遍历时,就会造成无限死循环。
分析到这里我们已经清楚多线程下扩容操作会造成死循环的原因,有人曾经把这个问题作为bug提给jdk官方,但是jdk并不认为这是个Bug。其实我也不认为这是个bug,人家已经很清楚的解释了,hashmap只适用单线程场景下,你硬要把他在多线程下去用,那会有各种各样的问题不能怪别人,就好比,你买的汽车,人家明明和你说了汽车是在公路上跑的,你偏要开车去水库里跑,发动机进水出了问题不能说人家汽车质量有问题吧。
解决方案
HashMap死循环的常用解决方案有以下3个:
- 使用线程安全容器ConcurrentHashMap替代(推荐使用此方案)。
- 使用线程安全容器Hashtable替代(性能低,不建议使用)。
- 使用synchronized或Lock加锁HashMap之后,再进行操作,相当于多线程排队执行