HashMap循环死链问题(hashmap infinite loop)

334 阅读2分钟

假设

  • hash表数组长度为4
  • hash算法是key % size,即哈希桶的索引bucketIndex = key % size
  • hash表中现存在3个元素
    key:17 bucketIndex = 17 % 4 = 1
    key:9 bucketIndex = 9 % 4 = 1
    key:3 bucketIndex = 3 % 4 = 3

image.png

public V put(K key, V value) {  
    if (table == EMPTY_TABLE) {  
        inflateTable(threshold);  
    }  
    if (key == null)  
        return putForNullKey(value);  
    int hash = hash(key);  
    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;  
}

当准备put元素key=5时,buckIndex = 5 % 4 == 1,触发扩容

触发扩容的条件:hash表的元素数量 >= 阈值 && put的元素hash桶不为空

void addEntry(int hash, K key, V value, int bucketIndex) {  
    if ((size >= threshold) && (null != table[bucketIndex])) {  
        resize(2 * table.length);  
        hash = (null != key) ? hash(key) : 0;  
        bucketIndex = indexFor(hash, table.length);  
    }  

    createEntry(hash, key, value, bucketIndex);  
}

void createEntry(int hash, K key, V value, int bucketIndex) {  
    Entry<K,V> e = table[bucketIndex];  
    table[bucketIndex] = new Entry<>(hash, key, value, e);  
    size++;  
}

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);  
}

oldTable的数据迁移到newTable

void transfer(Entry[] newTable, boolean rehash) {// 
    int newCapacity = newTable.length;  
    for (Entry<K,V> e : table) {  
        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;  
        }  
    }  
}

特别备注:本文所有图中都展示的是内层循环while执行一次的状态图,虚线条表示指针修改后的指向

单线程的扩容

image.png

image.png

扩容时使用头插法会倒置以前的链表元素的顺序

多线程的扩容(循环死链)

假设现在有thread1和thread2两个线程执行到这里

void transfer(Entry[] newTable, boolean rehash) {  
    int newCapacity = newTable.length;  
    for (Entry<K,V> e : table) {  
        while(null != e) {  
            Entry<K,V> next = e.next;// thread1先执行这里,线程挂起
            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挂起之后,线程1的扩容状态,如下图; 为了区分2个线程,用e1,next1代表线程1的指向,线程2同理

image.png

线程2和单线程扩容时是同理,因此不多赘述,线程2扩容结束的状态如下图

image.png

线程1重新拿回CPU执行权,继续向下执行,内层循环的第一个循环结束后的状态

image.png 此时,我们看到图中已经存在循环引用的情况的存在,内层循环的第二个循环结束后的状态

image.png 从上述例子,站在线程1的角度,next本应该是指向下一个元素,但是由于线程2的扩容,修改了指向关系,next不是指向下一个元素而是指向了前一个元素,导致无限循环下去。

总结:多线程并发访问情况下,发生循环死链问题的本质是:某个节点的next指向了链表中前一个节点,从而形成死循环