HashMap的循环链表图解

HashMap的循环链表图解

@author:Jingdai
@date:2021.03.20

复习HashMap的知识点,总是看到jdk1.7前在多线程操作时可能会出现循环链表问题,不是很理解,于是研究源码并画图终于搞懂,记录一下。

由于本人电脑只有jdk1.5和jdk1.8,所以以下分析均基于jdk1.5,jdk1.7应该一样。

循环链表发生的地方

循环链表发生在多个线程同时对HashMap进行put操作使HashMap 扩容的时候。下面看代码。

public V put(K key, V value)

public V put(K key, V value) {
    
    
	if (key == null)
	    return putForNullKey(value);
        
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    
    
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    
    
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

代码前面的循环是用来判断插入的 Entry 是否是已有的 key,是就进行替换,这里不是我们关注的,加入元素主要发生在倒数第二行的 addEntry 方法。下面看这个方法。

void addEntry(int hash, K key, V value, int bucketIndex)

void addEntry(int hash, K key, V value, int bucketIndex) {
    
    
	Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

见代码最后两行,如果加入元素到达了扩容阈值,就进行 resize 操作,传入新的容量为 2倍的数组长度,继续进入这个方法。

void resize(int newCapacity)

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);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}

代码的前几行判断 oldCapacity 是否已经是最大了,如是则不能扩容,这里看下面 Entry[] newTable = new Entry[newCapacity]; 这行代码,当满足扩容条件时,就创建一个大小为2倍数组长度的新的HashMap,然后调用 transfer 方法将旧的元素传递到新的HashMap中,最后将新HashMap传给table变量。主要的扩容操作就在 transfer 方法中,也是发生循环链表的地方,接着看。

void transfer(Entry[] newTable)

void transfer(Entry[] newTable) {
    
    
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
    
    
        Entry<K,V> e = src[j];
        if (e != null) {
    
    
            src[j] = null;
            do {
    
    
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);  
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

代码并不长,找到了循环链表发生的地方,我们具体分析怎么产生的循环链表。

循环链表产生分析

要了解循环链表如何产生,我们首先看 transfer 方法在单线程中是如何正常工作的,下面利用图解分析。

在这里插入图片描述

如图,为了简单,这里我们假设HashMap原本的数组大小为2,扩容后大小为4。实际中这种情况不会发生,但是原理一样。图左为原始HashMap,图右是新的HashMap,我们只关注下标为1的那个桶,其他不关心,同时假设元素1、2、3都会插入到新的HashMap的1号桶中。下面一行一行代码分析。

在这里插入图片描述

进入循环前,先执行 Entry<K,V> e = src[j]; ,这时 e 就指向元素1,然后使 src[j] = null; ,再将 next 指向 e 的下一个元素,即元素2。下面那一行 int i = indexFor(e.hash, newCapacity); 是根据 hash 算出插入桶的下标,我们都假设算出来是1,即插入到新的1号桶中,后面我就不分析这行代码了。同时,由于src[j] = null,对这三个元素来说,原始HashMap已经用不到了,后面的图也不画了(画图挺累的^ _ ^)。

在这里插入图片描述

然后将 e.next 指向 newTable[i] , 注意这里 newTable[i] 里面还没有元素,所以是 null,即使 e.next 指向 null ,然后使 newTable[i] = e; ,就将新HashMap的1号桶指向 e ,这时就已经把元素1插入到新HashMap中了,接着看。

在这里插入图片描述

如上图,接着就是将 enext 更新一下。

在这里插入图片描述

如上图,接着就会将元素2插入到列表中,然后更新e和next将元素3也插入到里面。插入元素2和元素3步骤一样,我就不画了。更新的和最后的图如下。

在这里插入图片描述

好了,懂了在单线程中如何扩容元素就可以分析循环链表了。还有要注意一点,在插入元素的时候使用的是头插法,所以我们在旧HashMap中元素的顺序是123,而到了新的HashMap中就变成了321。

继续多线程,假设两个线程都要插入一个元素,且都进入到了执行了resize方法,即执行到 addEntry 方法中的 if (size++ >= threshold) 时都判断为true,然后在各自的线程中创建一个新的 newTable ,然后进入到 transfer 方法。还是上面的例子,假设两个线程一起执行,最开始的样子如下图。

在这里插入图片描述

我们用 e1e2 分别代表线程1和线程2中的局部变量 e,用 next1next2 分别代表线程1和线程2中的局部变量 next

在这里插入图片描述

如上图,假设线程1和线程2执行到这步时,线程2失去了时间片,线程1继续执行直到结束,那么线程1执行结束后,就成了下面这样。

在这里插入图片描述

然后线程2获得了时间片,开始执行,接着一步一步分析。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

上面图是一步一步按代码走的,走到最后一步 enull 了,就退出了循环,此时已经有环了,对应于线程2,它对应的图就是下面这样。

在这里插入图片描述

如果线程2后将 newTable 写回 table ,环就产生了,同时发现元素3也丢失了,再也找不到了。

以上就是关于jdk1.7版本前HashMap生成环的一个例子,希望对大家有帮助。如果上面说的有什么错误也欢迎指正,一起学习进步。

猜你喜欢

转载自blog.csdn.net/qq_41512783/article/details/115028747