Java基础知识强化之集合框架笔记80:HashMap的线程不安全性的体现

1. HashMap 的线程不安全性的体现:数组

主要是下面两方面:安全

(1)多线程环境下,多个线程同时resize()时候,容易产生死锁现象。即:resize死循环数据结构

(2)若是在使用迭代器的过程当中有其余线程修改了map,那么将抛出ConcurrentModificationException,即:fail-fast策略多线程

 

2. resize死循环:并发

(1)为何会出现resize死循环函数

在单线程状况下,只有一个线程对HashMap的数据结构进行操做,是不可能产生闭合的回路的。那就只有在多线程并发的状况下才会出现这种状况,那就是在put操做的时候,若是size> initialCapacity * loadFactor,那么这时候HashMap就会进行rehash操做,随之HashMap的结构就会发生翻天覆地的变化。颇有可能就是在两个线程在这个时候同时触发了rehash操做,产生了闭合的回路。this

(2)下面咱们从源码中一步一步地分析resize死循环是如何产生的:spa

-->1. 存储数据put(K key, V value)线程

public V put(K key, V value) { ...... //算Hash值
    int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //若是该key已被插入,则替换掉旧的value (连接操做)
    for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //该key不存在,须要增长一个结点
  addEntry(hash, key, value, i); return null; }

当咱们往HashMap中put元素的时候,先根据key的hash值获得这个元素在数组中的位置(即下标),而后就能够把这个元素放到对应的位置中了。 若是这个元素所在的位置上已经存放有其余元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,而先前加入的放在链尾code

 

-->2. 检查容量是否超标addEntry

void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //查看当前的size是否超过了咱们设定的阈值threshold,若是超过,须要resize
    if (size++ >= threshold) resize(2 * table.length); }

能够看到,若是如今size已经超过了threshold,那么就要进行resize操做,新建一个更大尺寸的hash表,而后把数据从老的Hash表中迁移到新的Hash表中。

 

-->3. 调整Hash表大小resize

void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; ...... //建立一个新的Hash Table
    Entry[] newTable = new Entry[newCapacity]; //将Old Hash Table上的数据迁移到New Hash Table上
 transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }

当table[]数组容量较小,容易产生哈希碰撞,因此,Hash表的尺寸和容量很是的重要。通常来讲,Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的thredhold,若是超过,须要增大Hash表的尺寸,这个过程称为resize。

多个线程同时往HashMap添加新元素时,屡次resize会有必定几率出现死循环,由于每次resize须要把旧的数据映射到新的哈希表,多个线程执行resize操做都须要得到旧哈希表资源就会有概率出现死锁现象,而后相互等待,从而造成死循环。这一部分代码在HashMap#transfer() 方法,以下:

void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; //下面这段代码的意思是: // 从OldTable里摘一个元素出来,而后放到NewTable中
    for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do {  Entry<K,V> next = e.next;  int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }

大概看下transfer:

  • 对索引数组中的元素遍历
  • 对链表上的每个节点遍历:用 next 取得要转移那个元素的下一个,将 e 转移到新 Hash 表的头部,使用头插法插入节点
  • 循环2,直到链表节点所有转移
  • 循环1,直到全部索引数组所有转移

通过这几步,咱们会发现转移的时候是逆序的。假如转移前链表顺序是1->2->3,那么转移后就会变成3->2->1。这时候就有点头绪了.

死锁问题不就是由于1->2的同时2->1形成的吗?

因此,HashMap 的死锁问题就出在这个transfer()函数上

 

黄色部分代码是致使多线程使用hashmap出现CPU使用率骤增,从而多个线程阻塞的罪魁祸首

 

(3)总结:

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

 

 

3. fail-fast策略:

若是在使用迭代器的过程当中有其余线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略

首先咱们要知道迭代器是依附于对应的Map的,好比说:

在线程Thread1中,咱们得到map的迭代器iterator1,此时使用iterator1遍历map数据(Thread1运行中,没有结束)

在线程Thread2中,咱们修改了map,map变成了map1,好比时候添加元素(put)、或者删除元素(remove)。此时Thread1就会抛出ConcurrentModificationException错误。

修改了map,咱们迭代的map不是原来的map,可是竟然仍是使用依附原来的map的迭代器iterator1,去迭代map1,天然是不合理的。

相关文章
相关标签/搜索