zoukankan      html  css  js  c++  java
  • HashMap为什么线程不安全(hash碰撞与扩容导致)

      一直以来都知道HashMap是线程不安全的,但是到底为什么线程不安全,在多线程操作情况下什么时候线程不安全?

    让我们先来了解一下HashMap的底层存储结构,HashMap底层是一个Entry数组,一旦发生Hash冲突的的时候,HashMap采用拉链法解决碰撞冲突,Entry内部的变量:

    [java] view plain copy
     
    1. final Object key;  
    2. Object value;  
    3. Entry next;  
    4. int hash;  


            通过Entry内部的next变量可以知道使用的是链表,这时候我们可以知道,如果多个线程,在某一时刻同时操作HashMap并执行put操作,而有大于两个key的hash值相同,如图中a1、a2,这个时候需要解决碰撞冲突,而解决冲突的办法上面已经说过,对于链表的结构在这里不再赘述,暂且不讨论是从链表头部插入还是从尾部初入,这个时候两个线程如果恰好都取到了对应位置的头结点e1,而最终的结果可想而知,a1、a2两个数据中势必会有一个会丢失,如图所示:

    再来看下put方法

    [java] view plain copy
     
    1. public Object put(Object obj, Object obj1)  
    2.     {  
    3.         if(table == EMPTY_TABLE)  
    4.             inflateTable(threshold);  
    5.         if(obj == null)  
    6.             return putForNullKey(obj1);  
    7.         int i = hash(obj);  
    8.         int j = indexFor(i, table.length);  
    9.         for(Entry entry = table[j]; entry != null; entry = entry.next)  
    10.         {  
    11.             Object obj2;  
    12.             if(entry.hash == i && ((obj2 = entry.key) == obj || obj.equals(obj2)))  
    13.             {  
    14.                 Object obj3 = entry.value;  
    15.                 entry.value = obj1;  
    16.                 entry.recordAccess(this);  
    17.                 return obj3;  
    18.             }  
    19.         }  
    20.   
    21.         modCount++;  
    22.         addEntry(i, obj, obj1, j);  
    23.         return null;  
    24.     }  


    put方法不是同步的,同时调用了addEntry方法:

    [java] view plain copy
     
    1. void addEntry(int i, Object obj, Object obj1, int j)  
    2.     {  
    3.         if(size >= threshold && null != table[j])  
    4.         {  
    5.             resize(2 * table.length);  
    6.             i = null == obj ? 0 : hash(obj);  
    7.             j = indexFor(i, table.length);  
    8.         }  
    9.         createEntry(i, obj, obj1, j);  
    10.     }  

    addEntry方法依然不是同步的,所以导致了线程不安全出现伤处问题,其他类似操作不再说明,源码一看便知,下面主要说一下另一个非常重要的知识点,同样也是HashMap非线程安全的原因,我们知道在HashMap存在扩容的情况,对应的方法为HashMap中的resize方法:

    [java] view plain copy
     
    1. void resize(int i)  
    2.     {  
    3.         Entry aentry[] = table;  
    4.         int j = aentry.length;  
    5.         if(j == 1073741824)  
    6.         {  
    7.             threshold = 2147483647;  
    8.             return;  
    9.         } else  
    10.         {  
    11.             Entry aentry1[] = new Entry[i];  
    12.             transfer(aentry1, initHashSeedAsNeeded(i));  
    13.             table = aentry1;  
    14.             threshold = (int)Math.min((float)i * loadFactor, 1.073742E+009F);  
    15.             return;  
    16.         }  
    17.     }  


             可以看到扩容方法也不是同步的,通过代码我们知道在扩容过程中,会新生成一个新的容量的数组,然后对原数组的所有键值对重新进行计算和写入新的数组,之后指向新生成的数组。


            当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。

  • 相关阅读:
    【HDOJ】2267 How Many People Can Survive
    【HDOJ】2268 How To Use The Car
    【HDOJ】2266 How Many Equations Can You Find
    【POJ】2278 DNA Sequence
    【ZOJ】3430 Detect the Virus
    【HDOJ】2896 病毒侵袭
    求奇数的乘积
    平方和与立方和
    求数列的和
    水仙花数
  • 原文地址:https://www.cnblogs.com/baizhanshi/p/7724656.html
Copyright © 2011-2022 走看看