前言:我们都知道HashMap不是线程安全的,不建议在多线程环境下使用,但是它的线程不安全主要体现在哪里,本文将解密问题。1、jdk1.7中的HashMap对jdk1.8中的HashMap做了很多优化。这里我们先分析一下jdk1.7中存在的问题。相信大家都知道HashMap在jdk1.7多线程环境下容易死循环。这里我们先用代码来模拟死循环的情况:newHashMapThread();thread0.start();thread1.start();thread2.start();thread3.start();thread4.start();}}classHashMapThreadextendsThread{privatestaticAtomicIntegerai=newAtomicInteger();privatestaticMap<整数,整数>map=newHashMap<>();@Overridepublicvoidrun(){while(ai.get()<1000000){map.put(ai.get(),ai.get());ai.incrementAndGet();}}}上面的代码比较简单,就是开多个线程不断的进行put操作,HashMap和AtomicInteger都是全局共享的。多运行几次代码,出现如下死循环情况:其中有的还出现数组越界的情况:这里重点分析为什么会出现死循环,通过jps查看死循环情况和jstack命名,结果如下:从栈信息中可以看出死循环发生的位置。通过这些信息可以清楚的知道死循环发生在HashMap的扩展函数中。根本原因在于传递函数。jdk1.7中HashMap的传递函数如下:voidtransfer(Entry[]newTable,booleanrehash){intnewCapacity=newTable.length;for(Entrye:table){while(null!=e){Entrynext=e.next;if(rehash){e.hash=null==e.key?0:hash(e.key);}inti=indexFor(e.hash,newCapacity);e.next=newTable[i];newTable[i]=e;e=next;}}}总结一下这个函数的主要作用:将表扩展为newTable后,需要将原来的数据转移到newTable中,注意10-12行代码,这里我们可以看到在传递元素的过程中,采用了头部插入的方式,即链表的顺序会颠倒过来,这也是形成死循环的关键点.下面进行详细分析。1.1扩容导致的死循环分析过程前提条件:这里假设hash算法是简单keymod链表的大小。一开始哈希表size=2,key=3,7,5都在table[1]中。然后resize使size为4。resize前的数据结构如下:如果在单线程环境下,最后的结果如下:这里的传递过程就不详细描述了,只要明白什么就可以了传递函数是干什么的,传递过程和如何反转链表应该不难。那么在多线程环境下,假设有两个线程A和B在执行put操作。线程A在传递函数的第11行代码处挂了,因为这里对这个函数的分析很重要,所以重新贴出来。此时线程A中的运行结果如下:线程A挂起后,线程B正常执行,完成resize操作。newTable和table中的entry都是主存中的最新值:7.next=3,3.next=null。此时切换到线程A,当线程A挂起时,内存值如下:e=3,next=7,newTable[3]=null,代码执行过程如下:newTable[3]=e---->newTable[3]=3e=next---->e=7此时结果如下:继续循环:e=7next=e.next---->next=3【从主存取值】e.next=newTable[3]---->e.next=3【从主存取值】newTable[3]=e---->newTable[3]=7e=next---->e=3结果如下:再次循环:e=3next=e.next---->next=nulle.next=newTable[3]---->e.next=7即:3.next=7newTable[3]=e---->newTable[3]=3e=next---->e=null注意这个循环:e.next=7,并且在最后循环7.next=3,出现循环链表,此时e=null循环结束。结果如下:只要后续操作涉及到pollinghashmap这个数据结构,这里就会死循环,造成悲剧。1.2扩容导致数据丢失的分析过程如上分析过程。初始:线程A和线程B执行put操作,线程A挂了:此时线程A的运行结果如下:此时线程B已经获得了CPU时间片,并完成了resize操作:也请注意,由于线程B已执行完毕,因此newTable和table都有最新值:5.next=null。此时切换到线程A,此时线程A挂了:e=7,next=5,newTable[3]=null。执行newtable[i]=e,把7放到table[3]的位置,此时next=5。然后进行下一个循环:e=5next=e.next---->next=null,从主存取值e.next=newTable[1]---->e.next=5,从主存取值newTable[1]=e---->newTable[1]=5e=next---->e=null,将5放在table[1]位置。此时e=null循环结束,3个元素丢失,形成循环链表。并在后续操作hashmap时造成死循环。2、jdk1.8中的HashMap对jdk1.8中的HashMap进行了优化。当发生hash碰撞时,不再使用头部插入的方式,而是直接插入链表的尾部,所以不会出现循环链表,但是在很多线程的情况下,还是不安全的。这里我们看一下jdk1.8中HashMap的put操作的源码:,i;if((tab=table)==null||(n=tab.length)==0)n=(tab=resize()).length;if((p=tab[i=(n-1)&hash])==null)//如果没有hash碰撞,直接插入元素tab[i]=newNode(hash,key,value,null);else{Nodee;Kk;if(p.hash==hash&&((k=p.key)==key||(key!=null&&key.equals(k))))e=p;elseif(pinstanceofTreeNode)e=((TreeNode)p).putTreeVal(this,tab,hash,key,value);else{for(intbinCount=0;;++binCount){if((e=p.next)==null){p.next=newNode(hash,key,value,null);if(binCount>=TREEIFY_THRESHOLD-1)//-1for1sttreeifyBin(tab,hash);break;}if(e.hash==hash&&((k=e.key)==key||(key!=null&&key.equals(k))))break;p=e;}}if(e!=null){//existingmappingforkeyVoldValue=e.value;if(!onlyIfAbsent||oldValue==null)e.value=value;afterNodeAccess(e);returnoldValue;}}++modCount;if(++size>threshold)resize();afterNodeInsertion(evict);returnnull;}这个是jdk1.8中HashMap中put操作的主要功能,pay注意第六条如果没有hash碰撞,则直接插入元素。如果线程A和线程B同时进行put操作,正好两个不同数据的hash值相同,这个位置的数据为null,所以线程A和B都会进入第一个在6行代码中。假设一种情况,线程A进入后数据插入前挂掉,线程B正常执行,从而正常插入数据,然后线程A获得CPU时间片。这时线程A不再需要进行hash判断,问题就来了:线程A会覆盖线程B插入的数据,导致线程不安全。这里只是简单分析一下jdk1.8中HashMap线程不安全的表现。后面会总结Java集合框架,再具体分析。总结首先,HashMap不是线程安全的,其主要表现有:在jdk1.7中,在多线程环境下,扩容会造成环链或数据丢失。在jdk1.8中,在多线程环境下,会出现数据覆盖。