Hashmap jdk1.7死循环问题

发布时间 2023-09-11 21:35:05作者: zhangyukun

hashmap是在jdk1.7是数组+链表,通过hash计算出数组下标位置以后,如果同一个位置有多个元素,放在链表中,在多线程插入,并同时扩容的并发环境会出现死循环问题

头插入法

在维护链表元素的过程中,有一个head指针,指向第一个元素,没有尾部指针(未插入需要维护一个尾部指针,才能快递定位在哪里插入)。

举例子:A->B->C 这样一个结构使用头插入一个新元素D以后D->A->B->C

如果正好要扩容,那么久先扩容然后再插入,扩容也是头插入发

扩容以后变成
A->B->C 变成 C->B->A ,然后再插入 D ,D->C->B->A

单线程情况扩容过程

arrayOld ,arrayNew 两个集合,外层循环遍历数组,内层遍历链表

链表结构A->B->C

内循环过程

初始e = A;当前第一个节点

next = e.next;备注B保存B和后面的链表。

e.next = arrayNew[hash];把A的next指向新的集合对应的链,第一次是null。

arrayNew [hash] = e;新集合的第一个元素指定为新加入的这个当前节点

e=next;改变循环变量,next是B然后开始第二次内循环

...

直到 C.next 是 null 退出循环,结果就是顺序倒过来

死循环产生过程

两个线程要插入元素,hash值相等,同一个下标,达到扩容条件,先触发扩容,然后触发插入,实际上插入我们都不关心了,扩容的过程多线程就有可能出现死循环。

假设还是 A->B->C

线程1,扩容进行到一半并且保存部分变量

​ 初始e = A;

​ next = e.next;备注B

​ 线程暂停......然后线程2执行玩扩容以后它才会继续,这时候 e=A,next=B 是放在本地方法栈中,我们先去看线程2

​ e.next = arrayNew[hash];把A的next指向性的集合,第一次是null。

​ arrayNew [hash] = e;新集合的第一个元素指定为A

​ e=B

​ 这时候新集合是 A

第二轮循环

e=B;

next=e.next;线程2已经 把原链表改成C->B->A了,所以这时候是A,如果不出线程安全问题,它应该是C

e.next = arrayNew[hash];这时候新集合链表已经是B->A了

arrayNew [hash] = e;新集合的第一个元素指定为B
e=next,next这时候是A,不是C,因为取到的值是线程修改以后的当前值

第三轮循环,这里是关键因为这里的e不是C而是A,把它作为对应hash位置的第一个节点,并且把它指向B,和第二轮的B->A 的链路就形成了环。

e=A;

next=A.next;是null;

e.next= arrayNew[hash];第二次循环的结果就是新集合链表已经是B->A了然后 a.next指向 B->的链表 ,结果就是A<->B,已经出问题了。

arrayNew [hash] = e;这时候把头节点指向A 这时候新集合的这个下标的链表 头节点指向A ,然后AB成环。

e=next;null退出循环。

线程2,直接扩容完成,容量倍增是原数据的2倍,但是当前生效的是和线程1 共用的。

​ 线程2 执行完成以后数组里面的链表结果C->B->A

结论:hashmap本来就非线程安全的,在多线程环境本来就不应该用它,即便不出死循环问题,也会出别的问题。这并不是一个bug,而是hashmap本来就没有支持多线程环境。新版本使用为插入不会改变顺序不会出现死循环,但是多线程插入还是会数据不一致。