HashMap的死锁(JDK7)

4 分钟读完

说明

早期JDK的HashMap,在多线程下,会出现各种问题:

  • 数据掉了
  • 数据删了又回来了
  • 死锁

这里分析死锁产生的原理(或者说可能性)之一

源码

http://hg.openjdk.java.net/icedtea/jdk7/jdk/file/tip/src/share/classes/java/util/HashMap.java

我们需要关注的是函数transfer下的while循环(行4-13)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
    }
  }
}

它的主要逻辑:

  • 获取一个节点,
  • 计算节点的hash值,以及对应在新数组中的slot位置
  • 查看slot位置上,是否已经有节点链:
    • 如果没有,直接将新节点放入;
    • 如果原来的位置上已有节点,将旧节点附加在新节点的后面,新节点成为链条的头部(因此叫头插法),新节点放在slot位置上;
  • 如果有剩余节点,继续循环

案例设定

假设有table数组长度是2,有俩个元素,key的hashCode分别是【3】和【7】。
原始的数据结构如下:

1
2
0
1   【3】-> 【7】

现在要将其transfer为长度4的数组。

正常的程序运行过程

程序逻辑认定的正常运行过程如下:

  • while循环第一次运行到最后
1
2
3
4
5
6
0
1 	
2
3   【3】
-- 以上是数组,以下是临时变量 --
e  =【7】
  • 循环第二次运行到最后
1
2
3
4
5
6
0
1
2
3   【7】 -> 【3】

e = null
  • e = null,整体循环结束

死锁的产生

假设我们有俩个线程,同时运行了transfer操作(这个非常容易发生)。

  • 俩个线程都完成第一个循环

俩个线程中数据结构变为:

1
2
3
4
5
6
0
1 	
2
3   【3】

e  =【7】
  • 线程1停留在这里,线程2继续完成第二个循环,结束退出

线程2里面看见的数据结构是这样

1
2
3
4
5
6
0
1
2
3   【7】 ->【3】

e = null

根据Java内存模型,节点之间的next指向关系会影响到线程1

因此,线程1里面的数据结构就变成了:

1
2
3
4
5
6
0
1 	
2
3   【3】

e  =【7】->【3】
  • 线程1的二次循环结束
1
2
3
4
5
6
0
1 	
2
3   【7】->【3】

e  =【3】

这个时候,e != null,还会继续循环

  • 线程1的三次循环结束
1
2
3
4
5
6
0
1
2
3   【3】<->【7】

e = null
  • e = null,整体循环结束
  • 节点【3】【7】形成相互循环

死锁的爆发

查询操作的key位于slot3上时候(例如hashCode是【11】),将产生死循环

问题分析

这个问题的根源,在于:

  • 发生问题的key,它们在tranfer到新的数组后还位于一个slot上
  • 链表的头插法
  • 多线程

链表的头插法是其中的关键要素之一,这个不修改,问题就始终存在。下面是jdk7中HashMap的其它版本,这个问题依旧存在。

https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/HashMap.java

标签:

分类:

更新时间:

留下评论