多线程下HashMap的死循环是如何产生的

前言

  HashMap不是线程安全的,如果需要在多线程环境中使用Map,那么我们可以使用ConcurrentHashmap。

1.举例说明:

package com.test;

import java.util.HashMap;
import java.util.UUID; public class Test {
public static void main(String[] args) throws InterruptedException { final HashMap<String, String> map = new HashMap<String, String>(2); for (int i = 0; i < 10000; i++) { new Thread(new Runnable() { @Override
public void run() {
System.out.println(UUID.randomUUID().toString());
map.put(UUID.randomUUID().toString(), "");
} }, "线程Thread-" + i).start(); } }
}

  执行结果:

  通过运行该段代码,过一段时间之后就会显示CPU使用率100%。

2.分析原因:

  毫无疑问,因为陷入了死循环所以才会出现CPU使用达到100%的情况。可是为什么会出现死循环呢?知己知彼百战百胜,所以我们需要知道HashMap的原理所在,看源码。

  2.1 HashMap结构

  HashMap通常会用一个指针数组(假设为table[])来做分散所有的key,当一个key被加入时,会通过Hash算法通过key算出这个数组的下标i,然后就把这个<key, value>插到table[i]中,如果有两个不同的key被算在了同一个i,那么就叫冲突,又叫碰撞,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,而先前加入的放在链尾,这样会在table[i]上形成一个链表。最坏的情况下,所有的key都映射到同一个桶中,这样hashmap就退化成了一个链表——查找时间从O(1)到O(n)。Hash表这个容器当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的无素都需要被重算一遍。这叫rehash,这个成本相当的大。

如代码所示:

  

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);
//查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize
if (size++ >= threshold)
resize(2 * table.length);
}

  2.2 resieze() 操作

  如代码所示,如果现在size已经超过了threshold,那么就要进行resize操作,新建一个更大尺寸的hash表,然后把数据从老的Hash表中迁移到新的Hash表中:

resize(newCapacity):
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);
}

  

transfer(Entry[] newTable):
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;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}

  2.3冲突产生

  注意代码:

  

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

  如果有两个线程同时访问到这个循环操作,也就是说两个线程同时触发了rehash()操作之后:(对于这个例子假设初始容量为2,使用的Hash算法是取摸计算)

多线程下HashMap的死循环是如何产生的

  首先需要说明的是线程T1和线程T2同时访问到上面的代码,T1指向当前对象e,然后被挂起所以T2指向了Next对象,这也是为什么扩容后看到链表的顺序被反转。在第三i部就会形成一个局部链表,但我们在这个Map中放入元素11的时候会寻找到table[3]这个位置,然后就会陷入无休止的死循环中,这样,就不难解释CPU达到100%的原因了。(PS:图画的很丑)

3.其他扩展

    针对HashMap退化成单链表的问题,在JDK8中有所改善。如果某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap实现来替换掉它。这样做的结果会更好,查取元素时的花费是O(logn),而不是糟糕的O(n)。它是如何工作的?前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。这对HashMap的key来说并不是必须的,不过如果实现了当然最好。

上一篇:状态压缩codeforces 11 D


下一篇:Important Programming Concepts (Even on Embedded Systems) Part V: State Machines