当前位置: 首页 > 后端技术 > Java

在多线程环境下,HashMap为什么会死循环?

时间:2023-04-01 23:45:42 Java

Java的HashMap不是线程安全的,ConcurrentHashMap应该在多线程下使用。[HashMap]在多线程下的问题(这里主要指死循环问题):多线程put操作后,get操作导致死循环。多线程put非NULL元素后,get操作获取NULL值。多线程put操作,导致元素丢失。1、为什么会出现死循环?多线程下使用非线程安全的HashMap,单线程完全不会出现。HashMap使用链表来解决Hash冲突。因为是链表结构,所以很容易形成闭环。这样只要在循环过程中有线程去获取HashMap,就会发生死循环。在单线程的情况下,只有一个线程对HashMap的数据结构进行操作,不可能产生闭环。那么只有在多线程并发的情况下才会出现这种情况,也就是在put操作的时候,如果size>initialCapacity*loadFactor,那么这个时候HashMap就会进行rehash操作,然后HashMap的结构就会发生地球——翻天覆地的变化。很有可能此时两个线程同时触发了rehash操作,导致闭环。2.它是怎么来的?存储数据put():publicVput(Kkey,Vvalue){......//计算Hash值inthash=hash(key.hashCode());inti=indexFor(hash,table.length);//如果key已经插入,替换旧值(链接操作)for(Entrye=table[i];e!=null;e=e.next){Objectk;if(e.hash==hash&&((k=e.key)==key||key.equals(k))){VoldValue=e.value;e.value=值;e.recordAccess(这个);返回旧值;}}modCount++;//key不存在,需要添加节点addEntry(hash,key,value,i);返回空值;hash值得到元素在数组中的位置(也就是下标),然后就可以把元素放到对应的位置了。如果这个元素所在的位置已经存储了其他元素,那么同位置的元素将以链表的形式存储,新加入的元素放在链头,之前加入的元素元素放在链的末尾。检查容量是否超标addEntry:voidaddEntry(inthash,Kkey,Vvalue,intbucketIndex){Entrye=table[bucketIndex];table[bucketIndex]=newEntry(hash,key,value,e);//检查当前大小是否超过了我们设置的阈值threshold,如果超过了,需要resizeif(size++>=threshold)resize(2*table.length);}如果当前大小已经超过了阈值,那么就必须进行resize操作,创建一个更大的哈希表,然后将数据从旧的哈希表迁移到新的哈希表中。调整哈希表的大小resize:voidresize(intnewCapacity){Entry[]oldTable=table;intoldCapacity=oldTable.length;......//创建一个新的HashTableEntry[]newTable=newEntry[newCapacity];//将旧哈希表上的数据迁移到新哈希表transfer(newTable);表=新表;阈值=(int)(newCapacity*loadFactor);}当table[]数组的容量较小时,容易产生希腊碰撞,因此Hash表的大小和容量非常重要。一般来说,当有数据要插入到Hash表容器中时,它会检查容量是否超过了设定的阈值。如果超过,则需要增加哈希表的大小。此过程称为调整大小。当多个线程同时向HashMap中添加新元素时,多次resize会有一定概率死循环,因为每次resize都需要将旧数据映射到新的hash表中。这部分代码在HashMap#transfer()方法中。如下:voidtransfer(Entry[]newTable){Entry[]src=table;intnewCapacity=newTable.length;//下面代码的意思是://从OldTable中取出一个元素放到NewT??able中for(intj=0;je=src[j];if(e!=null){src[j]=null;do{Entrynext=e.next;//获取第一个元素inti=indexFor(e.hash,newCapacity);e.next=newTable[i];新表[i]=e;e=下一个;}while(e!=null);}}}红色标记的代码是在使用多线程hashmap时导致CPU使用率突然升高和死循环的罪魁祸首,从而阻塞了多个线程。另外推荐:Java进阶视频资源3.图解HashMap死循环:普通的ReHash过程(单线程):假设我们的hash算法只是单纯的使用keymod看表的大小(也就是数组的长度)).最上面的是旧的哈希表。Hash表的大小为2,所以key=3,7,5。mod2后,都在table[1]中发生冲突。接下来的三步是将Hash表resize为4,然后rehash所有的过程。并发下的rehash(多线程)1)假设我们有两个线程。做{Entrynext=e.next;//<--假设线程在这里一执行就被调度挂起,执行其他操作inti=indexFor(e.hash,newCapacity);e.next=newTable[i];新表[i]=e;e=下一个;}while(e!=null);我们的线程二执行完成。于是我们就有了下面的样子:注意因为Thread1的e指向key(3),next指向key(7),线程二rehash后指向线程二重组后的链表。我们可以看到链表的顺序是颠倒的。这里线程一就变成了线程二操作的HashMap。另外,多线程系列的面试题和答案都整理好了。微信搜索Java技术栈,后台发送:面试,可以在线阅读。2)一旦线程被安排回来执行。首先执行newTalbe[i]=e;然后e=next,导致e指向key(7),下一个循环中的next=e.next导致next指向key(3)。3)一切都很好。线程一然后工作。取下键(7),放在newTable[i]的第一个,然后把e和next往下移。该元素所在位置已经有其他元素存储,那么相同位置的元素将以链表的形式存储,新加入的放在链头,之前加入的放在链表中放置在链的末端。4)出现循环链接。e.next=newTable[i]导致key(3).next指向key(7)。注意:此时key(7).next已经指向了key(3),循环链表就这样出现了。于是,我们的线程一调用HashTable.get(11),悲剧就发生了——死循环。这里介绍一下为什么HashMap在多线程下会死循环,但是在真实的生产环境中,不会使用线程不安全的HashMap。