在咱们开发程序过程当中,hashMap算是咱们最经常使用的数据结构了,那么若是咱们在高并发下使用hashMap可能会出现什么问题呢?java
一、拿到的结果不是咱们想要的。(非线程安全)编程
二、扩容而致使程序死循环。导致CPU100%;(JDK1.7版本扩容,1.8暂无此问题。严重)数组
为何会出现死循环,接下来咱们进行分析一下。首先咱们了解下hashMap的源码,以及put操做。安全
1.1.1 构造函数解析微信
hashMap代价都比较熟悉了,这里就简单介绍HashMap几个关键点。HashMap的数据结构就是数组+链表的数据结构,以下数据结构
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构 int hash;//对key的hashcode值进行hash运算后获得的值,存储在Entry,避免重复计算 /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
当实例化一个HashMap时,系统会建立一个长度为Capacity的Entry数组,这个长度被称为容量(Capacity),在这个数组中能够存放元素的位置咱们称之为“桶”(bucket),每一个bucket都有本身的索引,系统能够根据索引快速的查找bucket中的元素。 每一个bucket中存储一个元素,即一个Entry对象,但每个Entry对象能够带一个引用变量,用于指向下一个元素,所以,在一个桶中,就有可能生成一个Entry链。 Entry是HashMap的基本组成单元,每个Entry包含一个key-value键值对。 多线程
一个长度为16的数组中,每一个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。通常状况是经过hash(key)%len得到,也就是元素的key的哈希值对数组长度取模获得。好比上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。因此十二、2八、108以及140都存储在数组下标为12的位置。并发
在存储一对值时(Key—->Value对),其实是存储在一个Entry的对象e中,程序经过key计算出Entry对象的存储位置。换句话说,Key—->Value的对应关系是经过key—-Entry—-value这个过程实现的,因此就有咱们表面上知道的key存在哪里,value就存在哪里。函数
构造函数里的参数值高并发
//默认初始化化容量,即16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量,即2的30次方 static final int MAXIMUM_CAPACITY = 1 << 30; //默认装载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化以前的状态 static final Entry<?,?>[] EMPTY_TABLE = {}; //空的存储实体 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //实际存储的key-value键值对的个数 transient int size; //阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,threshold通常为 capacity*loadFactory。HashMap在进行扩容时须要参考threshold int threshold; //负载因子,表明了table的填充度有多少,默认是0.75,当超过容量*负载因子时会进行扩容 final float loadFactor; //用于快速失败,因为HashMap非线程安全,在对HashMap进行迭代时,若是期间其余线程的参与致使HashMap的结构发生变化了(好比put,remove等操做),须要抛出异常ConcurrentModificationException transient int modCount;
1.2.2 put源码分析
一、当咱们将一个元素放入hashMap中,首先判断map是否为空,若是是空,对数组进行填充
二、若是Key为null,则将值存储在table[0]的位置或table[0]的链表冲突连上
三、对key值再进行hash散列,使其散列均匀,而后经过indexFor进行肯定table的位置,该方法为h & (length-1);就是拿当前hash的值如table长度-1作与运算,其实本质等于hash值除table长度取余。
四、而后循环遍历链表,查看链表里的key是否存在,存在的话替换旧值。而后return 老的值
五、若是不存在,则在尾部添加新的entry节点。
public V put(K key, V value) { //若是table数组为空数组{},进行数据填充 if (table == EMPTY_TABLE) { inflateTable(threshold);//分配数组空间 } //若是key为null,存储位置为table[0]或table[0]的冲突链上 if (key == null) return putForNullKey(value); int hash = hash(key);//对key的hashcode进一步hash计算,确保散列均匀 int i = indexFor(hash, table.length);//获取在table中的实际位置 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //若是该对应数据已存在,执行覆盖操做。用新value替换旧value,并返回旧value Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this);//调用value的回调函数,其实这个函数也为空实现 return oldValue; } } modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败 addEntry(hash, key, value, i);//新增一个entry return null; }
1.2.3 addEntry节点方法以及扩容
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//当size超过临界阈值threshold,而且即将发生哈希冲突时进行扩容,新容量为旧容量的2倍 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length);//扩容后从新计算插入的位置下标 } //把元素放入HashMap的桶的对应位置 createEntry(hash, key, value, bucketIndex); } //建立元素 void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; //获取待插入位置元素 table[bucketIndex] = new Entry<>(hash, key, value, e);//这里执行连接操做,使得新插入的元素指向原有元素。 //这保证了新插入的元素老是在链表的头 size++;//元素个数+1 }
一、当咱们对要插入的元素定好位置以后,则须要对当前链表进行判断是否须要扩容
二、当前table数组长度大于threashold(表默认长度*负载因子)的时候,则须要对数组进行扩容,扩容方式为当前的数组长度的两倍
三、当数组扩容完毕,须要从新计算全部的元素的位置,而后插入对应的位置当中。扩容方法主要为resize以及resize中的transfer方法,不在细说
如图
扩容前,3,7,5对数组长度2取摩都是值1,因此都是在1的位置.
扩容后数组table长度为4,7,3取摩后为位置为3,5取摩为位置为1
四、最后执行createEntry方法,将生成的节点插入对应位置的头部。
了解了hashMap中put以及扩容操做以后,咱们模拟一个场景。有原hashMap以下
在多线程环境下有线程一和线程二,同时对该map进行扩容执行扩容。
当线程一执行到以下代码时被挂起
此时线程1的数据结构为,table已经扩容完毕,从新计算每一个元素位置时被挂起,此时key为3的还在1位置,3.next为7。
这时,线程2完成扩容,扩容后的数据结构为
当线程1被调度回来执行以后,由于线程一执行的e.next =newTable[i];将key3插入到3号位置,同时3.next=key7。此时e=key3,next=key7;
当执行到e=next的时候,e=key7,next=key7;
而后开始下一轮while。但此时由于线程二已经将key=7的next设置为key3(问题就在这里,线程二执行的时候,key7.next已是null了,但这里线程一去执行的时候key7.next倒是key3)。因此当第二轮循环开始,执行next=e.next后,next = key3。
以后经过头插法,将key7插入key3以前。在执行完next=e.next 以后,e=key3,next=key3;
而后开始第三轮循环。e和next都是key3。因此根据头插法。key3又要插入key7以前,这就致使了key3.next为key7,key7.next为key3
因此当咱们去get值得时候,当定位到3的时候,就会产生死循环。致使永远拿不到数据。
HashMap之因此在并发下的扩容形成死循环,是由于,多个线程并发进行时,由于一个线程先期完成了扩容,将原Map的链表从新散列到本身的表中,而且链表变成了倒序,后一个线程再扩容时,又进行本身的散列,再次将倒序链表变为正序链表。因而造成了一个环形链表,当get表中不存在的元素时,形成死循环。在1.8当中,链表扩容转为红黑树,没有相关的问题。
其余阅读 并发编程专题,你们有问题能够加我微信哈~