HashMap同步问题

一、HahMap 的同步问题

    我们都知道 HashMap 是线程不安全的,多线程环境下,会造成数据脏读,其实 HashMap 还有可能发生死循环(循环链表),从而导致内存,CPU 飙升(100%)的情况,下面我们就来分析一下:

该问题的出现主要是 resize() 方法造成的,下面是  resize() 核心代码:

void resize(int newCapacity)
{
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ......
    //创建一个新的Hash Table
    Entry[] newTable = new Entry[newCapacity];
    //将Old Hash Table上的数据迁移到New Hash Table上
    transfer(newTable);  // 造成死循环的根本原因 
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}

resize()方法的本质就是创建新的Entry数组,将原Map中的元素重新计算位置,加入到新的Map中。虽然死锁的成因是扩充时调用resize()方法,但真正的产生是发生在倒数第三行的transfer()方法中。

void transfer(Entry[] newTable)
{
    Entry[] src = table;
    int newCapacity = newTable.length;
    //从OldTable将元素一个个拿出来,然后放到NewTable中
    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;
                //计算节点在新的Map中的位置
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

下面是出现该问题的大致流程(图片来自大神的博客)

HashMap同步问题

1、假设hash算法就是简单的用key mod Entry数组的长度。注意e和next的指向,当并发resize()时,这两个指针对于死锁产生起着至关重要的作用。根据方法执行情况,原Map中的链表元素在新的Map中将顺序颠倒,如上图所示,经过一次resize()后key为7的节点排在了key为3的节点之前。
2、并非所有情况下都会产生死锁,这也需要线程在某一时刻的巧合。下面模拟死循环产生的过程:

do {
  Entry<K,V> next = e.next; //假设线程 A 执行至此被挂起,执行线程 B
  int i = indexFor(e.hash, newCapacity);
  e.next = newTable[i];
  newTable[i] = e;
   e = next;
} while (e != null);

    HashMap同步问题

假设现在有两个线程,线程A 和 线程B, 线程A因为某些原因被挂起(在代码的 Entry<K,V> next = e.next; 这一行挂起),此时记录 e指向key为3的节点,next指向key为7的节点。线程B 正常执行,执行结果如下:

HashMap同步问题

然后,线程A被唤醒,在线程A的空间中 e指向key为3的节点,next指向key为7的节点,开始执行:

    a)执行:newTalbe[i] = e

    b)执行:e = next,e指向了key(7)

     下一次循环 next = e.next 的结果就是 next指向key(3)

HashMap同步问题

线程A继续执行,执行结果如下:

HashMap同步问题

再执行 e.next = newTable[i] 导致 key(3).next 指向了 key(7)。注意:此时的key(7).next 已经指向了key(3), 于是环形链表就这样出现了。

HashMap同步问题

对于多线程导致 HashMap 出现死循环的问题,最好的办法就是在高并发的情况下规避使用不安全的HashMap,方案如下:

1.使用HashTable和调用Collections工具类的synchronizedMap()方法达到线程安全的目的。但由于synchronized是串行执行,在访问量很大的情况下效率很低,不推荐使用。

2.使用JUC包下的ConcurrentHashMap类,这个类很好的解决了多线程环境下的并发问题

JDK1.8 之后改进了 HashMap ,当链表的长度超过 8 时,就会将链表转为 红黑树,同时改成了尾插,理论上不会出现环形链表。但是还是会有脏读的情况出现(未测试)。为了避免不必要的异常,在高并发的情况还是使用 ConcurrentHashMap。