• 如何解决在多线程环境下使用HashMap导致死循环的问题?
  • 发布于 1天前
  • 56 热度
    0 评论
  • 那场梦
  • 18 粉丝 65 篇博客
  •   
一.前言
在Java开发中,HashMap作为最常用的集合类之一,以其高效的查找和插入性能被广泛应用。然而在多线程环境下使用时可能会发生死循环场景下,导致CPU负载率飙升至100%,引发系统卡顿或崩溃。

哈希表的基本结构
HashMap 的底层数组称为桶(Bucket),每个桶存储哈希冲突的元素。当元素数量超过阈值(容量 × 负载因子)时,HashMap会触发扩容(resize)操作,将数组容量翻倍并重新分配元素。在JDK1.8中,当链表长度超过8时,会自动转换为红黑树以优化查询性能;当长度减少到6时,又会转回链表节省空间。

线程不安全的根源
HashMap设计之初就不是线程安全的集合类,主要体现在以下场景:
多线程并发修改时,可能导致链表形成环形结构(JDK1.7及之前)
扩容过程中元素迁移不当,引发死循环

哈希值计算冲突导致链表过长,查询效率退化至O(n)


二.导致 CPU 负载率高的具体原因分析
HashMap引发CPU负载过高的问题,本质上是线程安全问题导致的无限循环。
排查过程:
通过jstack命令获取线程快照,发现多个线程卡在HashMap.get()方法
分析堆栈信息,确认线程在遍历链表时陷入无限循环
检查代码发现使用了HashMap存储会话信息,且存在多线程并发修改
扩容死循环(JDK1.7 及之前)
在JDK1.7中,HashMap的扩容函数(transfer)采用头插法迁移元素。当两个线程同时触发扩容时,可能导致链表节点引用形成环形结构。此后对该链表的操作会陷入无限循环,持续占用CPU资源。
// JDK1.7 transfer方法关键代码
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; // 线程1在此处挂起
            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获取next节点后被挂起,线程2完成扩容并修改了节点引用关系,线程1恢复后会基于旧的引用继续处理,最终形成环形链表。每次get操作都会遍历这个环形链表,导致CPU使用率飙升。
备注:之所以选择使用头插法,是因为JDK的开发者认为,后插入的数据被使用到的概率更高,更容易成为热点数据,而通过头插法把它们放在队列头部,就可以使查询效率更高。

三.如何解决
前面提到,之所以会发生这个死循环问题,是因为在JDK 1.8之前的版本中,HashMap是采用头插法进行扩容的,这个问题其实在JDK 1.8中已经被修复了,改用尾插法。
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            elseif ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        elseif (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY; //堆代码 duidaima.com
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    elseif (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
线程安全替代方案
ConcurrentHashMap:JDK1.7采用分段锁机制,JDK1.8基于CAS操作和synchronized实现,支持高并发读写,性能远超Hashtable
Collections.synchronizedMap():通过包装HashMap实现线程安全,性能较差但兼容性好
ConcurrentSkipListMap:适用于需要排序的场景,并发性能优异
用户评论