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

作者: Sophia_dd35 | 来源:发表于2019-07-22 23:55 被阅读85次

一、背景

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

相关文章

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

    一直以来只是知道HashMap是线程不安全的,但是到底HashMap为什么线程不安全,多线程并发的时候在什么情况下...

  • 如何安全的使用HashMap

    为什么HashMap是不安全的 HashMap的线程不安全是由于它本身的机制造成的。HashMap的存储结构是由数...

  • 程序员之Map

    HashMap、HashTable、ConcurrentHashMap a.线程安全问题HashMap是线程不安全...

  • HashMap问答

    HashMap是不是线程安全? 不是线程安全的。 为什么不安全? 线程不安全的两个添加是,数据可共享、可修改。Ha...

  • [转]一文读懂HashMap

    本文准备从以下几个方面去讲解HashMap:1)HashMap源码详细分析2)HashMap为什么是线程不安全的?...

  • java基础面试题

    HashMap和ConcurrentHashMap区别 HashMap是线程不安全的,ConcurrentHash...

  • HashMap

    HashMap线程不安全线程不安全具体是指:多个线程同时访问,并且至少有一个线程修改了HashMap的数据结构,则...

  • HashMap为什么线程不安全

    hash碰撞与扩容导致 一直以来都知道HashMap是线程不安全的,但是到底为什么线程不安全,在多线程操作情况下什...

  • HashMap 与 ConcurrentHashMap

    HashMap 与 ConcurrentHashMap 一、背景: 线程不安全的HashMap:因为多线程环境下,...

  • 并发容器

    HashMap 为什么HashMap是线程不安全的?1、同时put碰撞导致数据丢失2、同时put扩容导致数据丢失3...

网友评论

    本文标题:为什么HashMap是线程不安全的?

    本文链接:https://www.haomeiwen.com/subject/hudslctx.html