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<K,V>[] 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<K,V>[] tab; Node<K,V> 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<K,V> 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<K,V>)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<K,V>[] resize() {
    
        // 1
        Node<K,V>[] 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        
        // 13
        table = newTab;
        
        // 14
        if (oldTab != null) {
        
            // 15
            for (int j = 0; j < oldCap; ++j) {
            
                // 16
                Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap);
                    
                    // 21
                    else { // preserve order
                        
                        // 22
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> 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<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

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

final Node<K,V> getNode(int hash, Object key) {
        // 1
        Node<K,V>[] tab; Node<K,V> 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<K,V>)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<K,V> 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<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        
        // 1
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        
        // 2
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            
            // 3
            Node<K,V> 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<K,V>)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<K,V>)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<K,V> e : table) {
            while(null != e) {
                // 1
                Entry<K,V> 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,

<pre style="margin: 0px; padding: 0px; background-color: rgb(255, 255, 255); color: rgb(0, 0, 0); font-family: "Source Code Pro"; font-size: 12pt;">ConcurrentHashMap是最好的选择,当然还有其他的方案,但是如果可以使用</pre>

ConcurrentHashMap,其他方案都不推荐。


参考资料:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349