本文分析是基于JDK1.7的HashMapjava
public class HashMapTest extends Thread {
private static HashMap<Integer, Integer> map = new HashMap<>(2);
private static AtomicInteger at = new AtomicInteger();
@Override
public void run() {
while (at.get() < 1000000) {
map.put(at.get(), at.get());
at.incrementAndGet();
}
}
public static void main(String[] args) {
HashMapTest t0 = new HashMapTest();
HashMapTest t1 = new HashMapTest();
HashMapTest t2 = new HashMapTest();
HashMapTest t3 = new HashMapTest();
HashMapTest t4 = new HashMapTest();
HashMapTest t5 = new HashMapTest();
t0.start();
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
for (int i = 0; i < 1000000; i++) {
Integer integer = map.get(i);
System.out.println(integer);
}
}
}
复制代码
反复执行几回,出现这种状况则表示死循环了:算法
由上可知,Thread-7因为HashMap的扩容致使了死循环。数组
1 void transfer(Entry[] newTable, boolean rehash) {
2 int newCapacity = newTable.length;
3 for (Entry<K,V> e : table) {
4 while(null != e) {
5 Entry<K,V> next = e.next;
6 if (rehash) {
7 e.hash = null == e.key ? 0 : hash(e.key);
8 }
9 int i = indexFor(e.hash, newCapacity);
10 e.next = newTable[i];
11 newTable[i] = e;
12 e = next;
13 }
14 }
15 }
复制代码
咱们先来看下单线程状况下,正常的rehash过程:安全
在单线程状况下,一切看起来都很美妙,扩容过程也至关顺利。接下来看下并发状况下的扩容。多线程
有两个线程,分别用红色和蓝色标注了。并发
在线程1执行到第5行代码就被CPU调度挂起(执行完了,获取到next是7),去执行线程2,且线程2把上面代码都执行完毕。咱们来看看这个时候的状态ide
注意::线程二已经完成执行完成,如今table里面全部的Entry都是最新的,就是说7的next是3,3的next是null;如今第一次循环已经结束,3已经安置稳当。spa
这个时候其实就出现了死循环了,3移动节点头的位置,指向7这个Entry;在这以前7的next同时也指向了3这个Entry。线程
出现问题的关键缘由:若是扩容前相邻的两个Entry在扩容后仍是分配到相同的table位置上,就会出现死循环的BUG。在复杂的生产环境中,这种状况尽管不常见,可是可能会碰到。code
下面来介绍下元素丢失的问题。此次咱们选取三、五、7的顺序来演示:
JDK 8 中采用的是位桶 + 链表/红黑树的方式,当某个位桶的链表的长度超过 8 的时候,这个链表就将转换成红黑树
HashMap 不会由于多线程 put 致使死循环(JDK 8 用 head 和 tail 来保证链表的顺序和以前同样;JDK 7 rehash 会倒置链表元素),可是还会有数据丢失等弊端(并发自己的问题)。所以多线程状况下仍是建议使用 ConcurrentHashMap
HashMap 在并发时可能出现的问题主要是两方面:
若是多个线程同时使用 put 方法添加元素,并且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 同样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖
若是多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在从新计算元素位置以及复制数据,可是最终只有一个线程扩容后的数组会赋给 table,也就是说其余线程的都会丢失,而且各自线程 put 的数据也丢失