Java HashMap源码深度分析

​在java语言中,HashMap是一个非常重要的数据结构,它被广泛用于存储具有key-value映射关系的数据,HashMap提供了高效的数据结构来实现key-value的映射;从HashMap的设计与实现中,我们可以学到很多巧妙的计算机思维,对我们日常工作中进行编码及方案设计存在很高的参考价值,学习和掌握HashMap就成了非常有必要的一件事情了。
学习HashMap,有这么几个关键问题需要搞明白:

  • HashMap的数据结构是什么样的,不同jdk版本架构是如何的;

  • HashMap关键属性,属性的含义及如何设置等问题;

  • HashMap的key-value插入流程是怎样的;

  • HashMap的数据查询流程是怎样的;

  • HashMap的扩容机制是怎么工作的;

  • HashMap的数据更新流程是什么样的(比如:删除);

  • HashMap的并发问题产生原因及正确的并发用法(比如并发环境下如何产生cpu 100%);

搞明白这几个关键问题,HashMap就算是掌握得差不多了,下面,从源码级来分析一下这些关键问题的答案是什么。

一、 HashMap的数据结构


HashMap数据结构

HashMap的数据结构由数组+链表组成,从java 8开始,会新增红黑树这种数据结构,也就是在java8中,链表超过一定长度后就会变为红黑树。

在下文的分析中,如果不是特别说明,都是指的是java 8中的HashMap。

二 、HashMap关键属性


有几个关键的属性需要我们知道:

  /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

默认的table的大小,table的大小只能是2的幂次方,这和HashMap的哈希槽寻址算法存在着很大的关系,当然,HashMap执行resize的时候也会得益于这个table数组的幂次方大小的特性,这些问题稍后再分析;

 /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

table的最大长度;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

默认的负载因子,负载因子用于扩容,当HashMap中的元素数量大于:capacity * loadFactor后,HashMap就会执行扩容流程。

 /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

当链表的长度超过一定长度之和,链表就会被升级为红黑树,用于解决因为哈希碰撞非常严重的情况下的数据查询效率低下问题,最坏情况下,如果没有引入红黑树的情况下,get操作的时间复杂度将达到O(N),而引入红黑树,最坏情况下get操作的时间复杂度为O(lgN);8是默认的链表树化阈值,当某个hash槽内的链表的长度超过8之后,这条链表将演变为红黑树。

 /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

红黑树也有可能会降级为链表,当在resize的时候,发现hash槽内的结构为红黑树,但是红黑树的节点数量小于等于6的时候,红黑树将降级为链表。

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

链表升级为红黑树还需要一个条件,就是table的长度要大于64,否则是不会执行升级操作的,会转而执行一次resize操作。

    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node[] table;

table和上文中的结构图中的数组对应。

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

下次数组扩容阈值,这个值是通过:capacity * loadFactor计算出来的,每次扩容都会计算出下一次扩容的阈值,这个阈值说的是元素数量。

三、 HashMap数据插入流程


下面来跟着源码来学习一下HashMap的put操作流程:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

首先需要注意的是hash这个方法:

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

如果key是null,则hash的计算结构为0,这里就有一个关键信息,如果一个HashMap中存在key为null的entry,那么这个entry一定在table数组的第一个位置,这是因为hash计算的结果直接影响了数据插入的hash槽位置,这一点稍后再看。
如果key不为null,则会拿key对象的hashCode来进行计算,这里的计算称为“扰动”,为什么要这样操作呢?本质上是为了降低哈希碰撞的概率,这里需要引出HashMap中定位哈希槽的寻址算法。
HashMap的table数组容量只能是2的幂次方,这样的话,2的幂次方的数有一个特性,那就是:hash & (len - 1) = hash % len,这样,在计算entry的哈希槽位置的时候,只需要位运算就可以快速得到结果,不需要执行取模运算,位运算的速度非常快,要比取模运算快很多。
回到上面的问题,为什么要对key对象的hashCode执行扰动呢?因为计算哈希槽位置的时候需要和table数组的长度进行&运算,在绝大部分场景下,table数组的长度不会很大,这就导致hashCode的高位很大概率不能有效参加寻址计算,所以将key的hashCode的高16位于低16位执行了异或运算,这样得到的hash值会均匀很多。
接下来继续看put操作的流程:

/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict
        // 1           
        Node[] tab; Node p; int n, i;
        
        // 2
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 3    
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        
        // 4     
        else {
            // 5
            Node e; K k;
            
            // 6
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            
            // 7    
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            
            // 8    
            else {
            
                // 9
                for (int binCount = 0; ; ++binCount) {
                
                    // 10
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        
                        // 11
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    
                    // 12 
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    
                    // 13
                    p = e;
                }
            }
            
            // 14
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
​
                return oldValue;
            }
        }
​
        // 15
        if (++size > threshold)
            resize();
​
        return null;
    }

下面根据注释的编号来看一下对应位置的含义:

  • (1)这里主要关注tab和p两个变量,tab是table数组的一个引用,p是当前拿到的Node引用,这个Node可能为null;

  • (2)这里将table赋值给了tab变量,并且判断了tab数组是否为空,如果为空,表示是首次执行put操作,table还没有被初始化出来,需要执行初始化操作,这里直接调用resize方法就可以完成初始化操作,关于resize,下一小节再重点分析。

  • (3)(n - 1) & hash计算出来的就是这个key对应的哈希槽,这个算法上面已经分析过,p变量拿到了当前哈希槽的头节点,并进行了判断,如果是null,则说明此时这个哈希槽内部没有哈希冲突,直接创建一个新的Node插入这个槽即可;

  • (4)此时,说明p变量不为null,这个时候问题比较复杂,这个p可能是一个链表头节点,也可能是一个红黑树根节点,这是结构上的可能性,接下来需要做的,就是判断p代表的结构上是否存在即将要插入的key,如果存在,则说明节点已经存在,执行更新操作即可,如果不存在,则需要执行插入操作,看接下来的流程;

  • (5)同样,e变量存储的是代表存储key的Node,可能为null,如果key压根没有被存储过,那么e最终就是null,否则就是存储key的Node的一个引用;

  • (6)这里是判断哈希槽的头结点是否是存储key的节点,这是典型的判断方法,先对比hash,然后对比key,对比key的时候要特别注意,除了使用“==”来进行比较,还使用了key对象的equals方法;如果判断通过,则e就指向了已经存在的代表存储key的Node;

  • (7)如果执行到这,说明p(哈希槽的头结点)不是代表存储key的Node,那么就要继续后面的流程,这里首先判断了一下p的结构,如果是TreeNode,说明p代表的是红黑树的头结点,那就是要红黑树的节点插入方法putTreeVal来进行,关于红黑树,后续再仔细学习,本文点到为止。

  • (8)执行到这里,说明p代表的是一条链表的头结点,需要在p这条链表中查找一下是否存在表示key的Node;

  • (9)开始迭代链表,来查找key;

  • (10)e此时表示的是p的next,如果e为null,说明链表迭代到了末尾,此时依然没有发现key,则说明链表中根本不存在key节点,直接把key节点插入到末尾即可;

  • (11)这里有一个判断,binCount如果超过了TREEIFY_THRESHOLD,则需要将链表升级为红黑树,通过treeifyBin方法来实现这个功能;

  • (12)如果e节点就是key节点,那么就可以结束了,e就是key节点的一个引用;

  • (13)p = e,就是将p向前移动,继续判断,简单的链表迭代;

  • (14)如果此时e不为null,说明链表中存在key节点,那么本次put操作其实是一次replace操作;

  • (15)执行到这里,说明put操作插入了一个新的Node,如果插入后HashMap中的Node数量超过了本次扩容阈值,那么就要执行resize操作,resize操作将在下一小节详细展开分析;

四、 HashMap扩容机制


扩容对于HashMap是一个很重要的操作,如果没有扩容机制,因为有哈希碰撞的发生,会使得链表或者红黑树的节点数量过多,导致查询效率较低。下面,就从源码的角度来分析一下HashMap的扩容是如何完成的:

/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node[] resize() {
    
        // 1
        Node[] oldTab = table;
        
        // 2
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        
        // 3 
        int oldThr = threshold;
        
        // 4
        int newCap, newThr = 0;
        
        // 5
        if (oldCap > 0) {
        
            // 6
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            
            // 7
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        
        // 8
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        
        // 9
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        
        // 10
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        
        // 11
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        
        // 12
        Node[] newTab = (Node[])new Node[newCap];
        
        // 13
        table = newTab;
        
        // 14
        if (oldTab != null) {
        
            // 15
            for (int j = 0; j < oldCap; ++j) {
            
                // 16
                Node e;
                
                // 17
                if ((e = oldTab[j]) != null) {
                
                    // 18
                    oldTab[j] = null;
                    
                    // 19
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    
                    // 20
                    else if (e instanceof TreeNode)
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    
                    // 21
                    else { // preserve order
                        
                        // 22
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node next;
                        do {
                            next = e.next;
                            
                            // 23
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            
                            // 24
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        
                        // 25
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        
                        // 26
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

resize操作很复杂,下面根据代码注释来看一下每个位置都在做什么:

  • (1)oldTab变量指向table,这个没有特别难以理解;

  • (2)oldCap就是当前table的大小,也就是扩容前的table大小,table可能未初始化,则oldCap就是0;

  • (3、4)变量赋值,新老容量和扩容阈值;

  • (5、6、7)oldCap大于0,则说明table已经初始化过,扩容的时候,新的容量是老的table的两倍,这里需要处理一下超过最大容量的问题,如果table数组已经达到最大了,那么就不要再继续扩容了,生死有命富贵在天吧
    嘿嘿

  • (8)这种情况下是说在创建HashMap的时候指定了初始化大小,那新的table的容量就是当前的扩容阈值;

  • (9)执行到这里,说明table还没创建,但是创建HashMap的时候没有指定初始容量,那么本次其实就是执行初始化table的工作;

  • (10)计算新的扩容阈值;

  • (11、12、13)创建好新的数组,大小是原来的两倍,并使用newTab表示;

  • (14)执行到这里,如果本次只是初始化table数组,那么其实resize的工作已经完成了,但是如果不是初始化table,而是执行正常的扩容操作,那么就需要执行数据迁移的工作,所谓数据迁移,就是将Node从原来的table中迁移到扩容出来的数组中;

  • (15)循环原来的table数组,逐个迁移数据;

  • (16)e变量用来表示当前遍历到的Node;

  • (17)原table数组中当前哈希槽可能是空的,如果是空的,就说明没有数据需要迁移,继续处理下一个哈希槽就可以,如果当前哈希槽有节点,那么就需要对当前哈希槽内的数据执行迁移操作;

  • (18)e变量已经拿到了当前需要迁移的哈希槽的头结点引用,执行oldTab[j] = null,就是为了减少引用,尽快让垃圾得到回收;

  • (19)这种情况很简单,当前需要迁移的哈希槽内部只有一个节点,那么就直接将该节点迁移到新的table中正确的位置就可以了;

  • (20)执行到这,表示哈希槽内是一颗红黑树,需要使用红黑树的节点迁移方法,这部分暂时不做分析;

  • (21)执行到这,说明需要迁移的是一条链表,下面就开始将这条链表上的节点迁移到新table中正确的位置上去;

  • (22)这里需要引出一个关键知识点,哈希槽数据迁移方案,得益于哈希数组的大小是2的幂次方这个特性,对于一个节点,在扩容后,它对应的哈希位置只可能存在两种情况,要么还是当前位置(在新数组中),要么是当前位置+oldCap;这是为什么呢?来看一个例子:

并发数据迁移

我们假设扩容前数组长度为2,则扩容后数组的长度为4,原数组的数组下标为1的位置上存在一条链表需要迁移到新数组中去,这条链表长度为3,根据哈希槽位置计算方法:hash & (len -1),原来的len = 2, len - 1 = 1,新的len = 4, len - 1 = 3;用二进制表示为:01 => 11,如果继续扩容,则(len - 1)的变化规则为:01 => 11 => 111 => 1111 => ...,可以看到,没扩容一次,hash值的高位就会多一位来参与哈希计算,多一位的这位hash数二进制表现为:要么是0,要么是1,只有这两种可能,如果为0,则相当于计算出来的哈希槽位置和原来一样,如果为1,则哈希槽位置会+oldCap,比如对于k1和k2,假设k1的hash计算结果为3,二进制表示为:11,则原来的下标为 (11 & 01) = b01 = 1,扩容后下标计算为:(11 & 11) = b11 = 3,3 = 1 + 2 = oldIndex + oldCap;有了这个知识点,那么就可以继续来看22这个位置上的代码了,这里有两组变量,loHead和loTail是一组,hiHead和hiTail是一组,这两组分别表示上面分析的两种情况,也就是loHead和loTail表示那些扩容后依然在原来下标的Node,hiHead和hiTail表示那些扩容后需要移动到oldIdex + oldCap位置上的Node;

  • (23)这里,如果e节点的hash&oldCap==0,说明本次新参与的高位二进制位0,那这个节点扩容后还是在当前的index;

  • (24)这里表示e节点扩容后需要移动到index + oldCap的位置上去;

  • (25、26)到这里,需要将两条链表放到新数组正确的位置上去,这样就完成了扩容操作

五、 HashMap数据查询机制


数据查询比较简单,我们来分析一下HashMap的get操作是如何完成的;

        public V get(Object key) {
        Node e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

上来首先计算了key的hash,然后在调用getNode来实现节点查找,接下来看一下getNode方法的实现;

final Node getNode(int hash, Object key) {
        // 1
        Node[] tab; Node first, e; int n; K k;
        
        // 2
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            
            // 3
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            
            // 4
            if ((e = first.next) != null) {
            
                // 5
                if (first instanceof TreeNode)
                    return ((TreeNode)first).getTreeNode(hash, key);
                
                // 6
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        
        // 7
        return null;
    }
  • (1)tab变量指向当前table,first是当前哈希槽的第一个节点的引用,e用于迭代链表;

  • (2)首先tab赋值,然后需要判断当前table是否为空,以及first赋值及检测是否为空,如果这些检测没通过,则说明当前HashMap中不可能存在key节点,直接返回null即可;

  • (3)如果first节点就是需要找到的key节点,则直接返回first节点;

  • (4)如果当前哈希槽只有一个节点,那么到此搜索结束,没有找到key节点,否则,继续在first结构上查找;

  • (5)如果当前槽内是一颗红黑树,则通过红黑树的查找方法来查找,这个分支暂时不看;

  • (6)否则,当前槽内就是一条链表,那么就需要迭代这条链表来找到目标节点;

  • (7)执行到这里,说明HashMap中不存在key节点;

六、 HashMap数据更新机制


数据更新操作主要看一下remove操作是如何实现的:

    public V remove(Object key) {
        Node e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

还是会先计算key的哈希值,然后调用removeNode方法来执行删除操作;下面来看一下removeNode方法的实现细节:

/**
     * Implements Map.remove and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to match if matchValue, else ignored
     * @param matchValue if true only remove if value is equal
     * @param movable if false do not move other nodes while removing
     * @return the node, or null if none
     */
    final Node removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        
        // 1
        Node[] tab; Node p; int n, index;
        
        // 2
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            
            // 3
            Node node = null, e; K k; V v;
            
            // 4
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            
            // 5
            else if ((e = p.next) != null) {
            
                // 6
                if (p instanceof TreeNode)
                    node = ((TreeNode)p).getTreeNode(hash, key);
                
                // 7
                else {
                    do {
                    
                        // 8
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            
            // 9
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                
                // 10
                if (node instanceof TreeNode)
                    ((TreeNode)node).removeTreeNode(this, tab, movable);
                
                // 11
                else if (node == p)
                    tab[index] = node.next;
                
                // 12
                else
                    p.next = node.next;
                ++modCount;
                
                // 13
                --size;
                return node;
            }
        }
        return null;
    }
  • (1)tab是当前table的引用,p指向定位到哈希槽的头结点,index是当前key节点的哈希槽位置;

  • (2)获取到p节点,如果p节点为null,说明当前HashMap中不可能存在key,所以删除失败,返回null,结束删除流程;

  • (3)node表示找到的key节点,也就是指向将要被删除掉的Node;

  • (4)这个分支表示当前槽内的头结点就是所要删除的Node,赋值给node变量;

  • (5)表示槽内的头节点并不是所要删除的节点,那么就要继续在p结构中查找,需要判断一些槽内是否就一个p节点,如果是,那么就可以结束删除流程,当前HashMap中不可能存在key节点;

  • (6)如果p结构是一颗红黑树,那么就要使用查找红黑树的方法查找节点;

  • (7)否则,就要在链表p中找到key节点;

  • (8)迭代整个p链表,找到key节点,并赋值给node变量,但可能没找到,此时node为null;

  • (9)如果node为null,则说明没有找到需要删除的数据,也就是不存在需要删除的节点,否则,就要执行删除操作;

  • (10)如果需要删除的node节点是一个红黑树节点,那么就调用红黑树的节点删除方法;

  • (11)如果node和p相等,那么就说明需要删除的节点是链表的头结点,只需要将头结点移动到next即可实现节点删除;

  • (12)否则,就要删除node节点,此时,p节点就是node节点的前一个节点,删除node节点只需要执行 p.next = node.next就可以实现;

  • (13)删除一个节点之后,需要将size减1;

七、 HashMap并发安全问题分析


我们都知道,HashMap是线程不安全的,所谓线程不安全,就是使用不当在并发环境下使用了HashMap,那么就会发生不可预料的问题,一个典型的问题就是cpu 100%问题,这个问题在java 7中是因为扩容的时候链表成环造成的,这个成环是因为java 7在迁移节点的时候使用的是头插法,在java 8中使用了尾插法避免了这个问题,但是java 8中依然存在100%的问题,在java 8中是因为红黑树父子节点成环了。
下面,来简单分析一下java 7中链表成环的问题:

 /**
     * 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) {
                // 1
                Entry next = e.next;
                
                // 2
                int i = indexFor(e.hash, newCapacity);
                
                // 3
                e.next = newTable[i];
                
                // 4
                newTable[i] = e;
                
                // 5
                e = next;
            }
        }
    }
image

如图所示,在扩容前,位置1上有一条长度为3的链表,扩容后数组的长度为4,这条链表的key=5的节点会被迁移到新数组位置为1的位置上,其余两个节点会按照尾插法迁移到新数组位置为3的位置上。
假设两个线程同时指向扩容,thread1执行到代码位置(1)的时候失去cpu,thread2此时获得cpu并完成了数据迁移,之后,thread1重新获得cpu,开始执行迁移操作,此时e执行key=3的节点,next指向key=7的节点,而此时thread2完成迁移后,key=7的节点的next为key=3的节点,此时已经成环,此时如果有线程执行get等查询操作,那么就可能陷入死循环;如果thread1可以继续执行迁移,执行注释中(3)这行代码后,就将key=3的节点的next指向了key=7的节点,执行(4)后,key=3的节点成了头结点,执行(5)后,e指向了key=7的节点,接着继续下一轮迁移;这样,这个迁移永远也完成不了,只会不断在更新槽位的头结点,死循环了。所以,如果是在并发环境下,我们应该使用线程安全的并发HashMap,

ConcurrentHashMap是最好的选择,当然还有其他的方案,但是如果可以使用

ConcurrentHashMap,其他方案都不推荐。


参考资料:

  • 为什么HashMap线程不安全 https://www.jianshu.com/p/e2f75c8cce01

  • java 8HashMap源码分析 https://www.jianshu.com/p/d5f0a99966f7

  • Java 7 HashMap put多线程并发操作导致cpu 100%

  • Java HashMap源码深度分析

你可能感兴趣的:(Java HashMap源码深度分析)