HashMap为什么线程不安全

  • HashMap的线程不安全体现在会造成死循环、数据丢失、数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7中出现的问题,在JDK1.8中已经得到解决,然而1.8中仍会有数据覆盖这样的问题。

1 扩容时出现的问题

  • 多线程同时扩容时,出现环形链表和数据丢失(JDK1.7)。扩容原理见:HashMap底层实现–扩容

  • 原因如下:

    JDK1.7中,在HashMap扩容时,会改变链表中的元素的顺序,将元素从链表头部插入。说是为了避免尾部遍历。

  • JDK1.7中扩容的transfer方法:

    JDK1.8 中扩容自己重写了扩容方法,resize()。
    1.7 是在 while 循环里面,单个计算好数组索引位置后,单个的插入数组中,在多线程情况下,会有成环问题。
    1.8 是等链表整个 while 循环结束后,才给数组赋值,所以多线程情况下,也不会成环。

    void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            for (Entry<K,V> e : table) {
                //e为空时循环结束
                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;
                }
            }
        }
    
分析成环过程:
  • 假设原来在数组 1 的下标位置有个链表,链表元素是 a->b->null,现在有两个线程同时执行这个方法,我们先来根据线程 1 的执行情况来分别分析下这三行代码。
  1. e.next = newTable[i];
    newTable 表示新的数组,newTable[i] 表示新数组下标为 i 的值,第一次循环的时候为null,e 表示原来链表位置的头一个元素,是 a,e.next 是 b。
    e.next = newTable[i] 的意思就是拿出 a 来,并且使 a 的后一个节点是 null,如下图 1 的位置:
    在这里插入图片描述

  2. newTable[i] = e;
    就是把 a 赋值给新数组下标为 1 的地方,如下图 2 的位置:
    在这里插入图片描述

  3. e = next;
    next 的值在 while 循环一开始就有了,为:Entry<K,V> next = e.next; 在此处 next 的值就是 b,把 b 赋值给 e,接着下一轮循环。

  4. 从 b 开始下一轮循环,重复 1、2、3,注意此时 e 是 b 了,而 newTable[i] 的值已经不是空了,已经是 a 了,所以 1,2,3 行代码执行下来,b 就会插入到 a 的前面,如下图 3 的位置:
    在这里插入图片描述
    这个就是线程 1 的插入节奏。

  5. 重点来了,假设线程 1 执行到现在的时候,线程 2 也开始执行。此时数组上面链表已经形成了 b->a->null。线程一的执行结果不仅仅是给newTable赋值,还改变了a、b两个对象的next的实际指向。使得b.next = a。所以此时线程二栈中的链表指向已经被修改成了:
    在这里插入图片描述

    注意newTable 是在扩容方法中新建的局部变量,newTable 不是共享的,线程 2 无法在线程 1 newTable 的基础上再进行迁移数据。

  • 总结:

    插入的时候和平时追加到尾部的思路是不一致的,是链表的头结点开始循环插入,导致插入的顺序和原来链表的顺序相反的。
    table 是共享的,table 里面的元素也是共享的,while 循环都直接修改 table 里面的元素的 next 指向,导致指向混乱。

2 值覆盖

  • JDK1.8源码put方法中:

    //如果没有hash碰撞则直接插入元素
    if ((p = tab[i = (n - 1) & hash]) == null) 
                tab[i] = newNode(hash, key, value, null);
    

    假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第二行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

  • JDK1.8源码put方法中:

    if (++size > threshold)
                resize();
    

    还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到++size时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。

  • JDK1.7中也有类似的值覆盖问题。

【参考文档】
大多数人不知道的:HashMap链表成环的原因和解决方案
HashMap中是如何形成环形链表的?
JDK1.7和JDK1.8中HashMap为什么是线程不安全的?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值