Java基础篇-HashMap多线程问题

常会说到HashMap在多线程下是不安全的,那么不安全会引起什么问题呢?

多线程下,对一个HashMap进行修改时,会造成元素丢失或者链表闭环。

1、HashMap的存储结构

首先看下HashMap的存储结构,HashMap的存储结构是Entry数组+链表的结构,如下图

2、先说一下元素丢失是怎么引起的

       上图中,两个线程分别插入元素g和h,经过hash计算,插入位置都是数组索引为3的链表中,g和h分别将到f的next指向自身(JDK8版本),g和h分别拿到尾结点f,g先将f的next指向g,接着h又将f的next做了修改指向h。最终,g节点丢失,f的next指向h。

       这是多线程下使用临界资源经常面临的一个问题,很容易理解。

3、那链表闭环又是怎么引起的呢,往链表中添加元素为什么会引起链表闭环呢?

    这需要先从HashMap的扩容说起了。

    HashMap是为了快速查询而存在的容器,为了能够快速查询,采用了通过空间换时间的方法。HashMap为了方便查询,使用了Hash处理,快速定位元素数组所在下标位置。常规状态下HashMap中数组长度永远大于元素个数的。

那么问题来了,数组长度是不可变的,如果元素数量超过loadFactor*capacity的数值,那么HashMap就需要重新申请一个新的数组,并把原来存储的元素转移到新的数组中。数组的长度变了,就需要重新计算元素的Hash位置。(不了解这块知识点的可以打开链接,看一下HashMap中Hash计算和Hash碰撞的问题)。


扩容这个过程中发生了什么呢,让我们一块看一下resize这个函数

void resize(int newCapacity) {

        Entry[] oldTable = table;

        int oldCapacity = oldTable.length;

        if (oldCapacity == MAXIMUM_CAPACITY) {

            threshold = Integer.MAX_VALUE;

            return;

        }

        Entry[] newTable = new Entry[newCapacity];

        transfer(newTable, initHashSeedAsNeeded(newCapacity));

        table = newTable;

        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);

    }

    /**

    * Transfers all entries from current table to newTable.

    */

    void transfer(Entry[] newTable, boolean rehash) {

        int newCapacity = newTable.length;

        for (Entry e : table) {

            while(null != e) {

                Entry 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这个函数很容易理解,就是申请了一个新的Entry数组newTable,进行空间扩容,然后接下来调用transfer 函数,将原来的数组中的链表节点元素,排列到新的Entry数组中。

在多线程下这个过程为什么会出现链表闭环呢?两个线程一起调用resize,分别对HashMap进行扩容。继续以这个图作为例子

假设HashMap现在容量是32,进行扩容到64,线程A和B都先申请一个新的64长度的数组,然后移动原来的元素,A先移动了f和g放到索引是n的位置,f的next指向g,B将元素f,g也放到索引是n的位置,不过是将g的next指向f。

那么现在出问题了,f的next指向g,g的next指向f,扩容完成。查询h元素,经过Hash计算,找到索引为n的数组元素,遍历链表查找f然后next操作一直在g和f之间循环,进入闭环链表中了,HashMap查询出问题了。

注意:JDK8以后HashMap使用红黑树,已经不会出现闭环的问题了,不过元素丢失还是没法避免。

HashMap会引起这些问题,最主要的原因是对临界资源的处理没有加锁。

4、解决方案

 ConcurrentHashMap

JDK7采用分片加锁的方案,支持多线程操作一个Map容器

JDK8采用红黑树+数组+链表的结构,通过synchronized+CAS保证了线程安全,并实现了多线程协助扩容,加快扩容速度

你可能感兴趣的:(Java基础篇-HashMap多线程问题)