其他零散知识点嘟嘟程序猿Java相关

为什么HashMap是线程不安全的?

2019-07-22  本文已影响85人  Sophia_dd35

一、背景

HashMap的扩容机制就是重新申请一个容量是当前容量2倍的桶数组,然后将旧数组中的元素逐个映射到新的数组里边,然后将原先的桶逐个置为引用失效。HashMap之所以线程不安全,就是resize这里出的问题。

二、线程不安全场景

1、put的时候导致的多线程数据不一致。
这个问题比较好想象,比如有两个线程A和B,首先A希望先插入一个key-value到HashMap中,首先计算key-value索引位置,然后获取到该桶里面链表的头节点,并且key不存在桶中,需要新建节点插入,此时线程A的时间片用完了;线程B被调度得以执行,和线程A一样执行,但线程B比较幸运,成功的将key-vaule插入到了链表中,随后线程A再次被调度运行时,它依然持有过期的链表头但它对此一无所知,如此一来就覆盖了线程B插入的记录,但线程B插入的记录就凭空消失了。

void createEntry(int hash, K key, V value, int bucketIndex) { 

    // 1. 把table中该位置原来的Entry保存  
    Entry<K,V> e = table[bucketIndex];

    // 2. 在table中该位置新建一个Entry:将原头结点位置(数组上)的键值对 放入到(链表)后1个节点中、将需插入的键值对 放入到头结点中(数组上)-> 从而形成链表
    // 即 在插入元素时,是在链表头插入的,table中的每个位置永远只保存最新插入的Entry,旧的Entry则放入到链表中(即 解决Hash冲突)
    table[bucketIndex] = new Entry<>(hash, key, value, e);  

    // 3. 哈希表的键值对数量计数增加
    size++;  
}

2、HashMap的get操作死循环
达到resize条件,两个线程各自进行resize的第一步-扩容。让我们回顾一下ReHash源码

//resieze的核心源码如下
//这个方法的功能时将原来的记录重新计算在新桶的位置,然后迁移过去
void transfer(Entry[] newTable, boolean rehash) {  
        int newCapacity = newTable.length;  
        for (Entry<K,V> e : table) {  
  
            while(null != e) {  
                Entry<K,V> next = e.next;          
                if (rehash) {  
                    e.hash = null == e.key ? 0 : hash(e.key);  
                }  
                int i = indexFor(e.hash, newCapacity);   
                e.next = newTable[i];  
                newTable[i] = e;  
                e = next;  
            } 
        }  
    }

从上面可看出:在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移数据操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况
此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态。


图片1.png 假如此时线程遍历到Entry3对象,刚执行完红圈里的这行代码,线程就被挂起 图片13.png

对于线程B来说e=Entry3,next=Entry2;
这时候线程A畅通无阻的进行着Rehash,当ReHash完成后,结果如下


图片2.png

接下来线程B恢复,继续执行属于它自己的ReHash,线程B刚才的状态是e = Entry3,next=Entry2
当执行红框内的一行代码时


图片8.png

显然i = 3,因为刚才线程A对于Entry3的hash结果也是3.
我们继续执行


图片9.png

Entry3放入线程B的数组下标为3的位置,并且e指向了Entry2。

接着新一轮的循环,又执行到


图片10.png

e = Entry2
next = Entry3


图片3.png

接下来执行


图片11.png

用头插法把Entry2插入到线程B的数组的头节点


图片4.png

接下来执行红框内的代码


图片5.png

e = Entry3
next = null
当我们执行下面这一行的时候,见证奇迹的时刻来临了:


图片6.png

newTable[i] = Entry2
e = Entry3
Entry2.next = Entry3
Entry3.next = Entry2
链表出现了环形!


图片12.png
上一篇下一篇

猜你喜欢

热点阅读