HashMap是非线程安全,死锁一般都是产生于并发情况下。
JDK7中的HashMap
HashMap底层维护一个数组,数组中的每一项都是一个Entry
transient Entry<K,V>[] table;
向 HashMap 中所放置的对象实际上是存储在该数组当中; 而Map中的key,value则以Entry的形式存放在数组中。
这个Entry应该放在数组的哪一个位置上(这个位置通常称为位桶或者hash桶,即hash值相同的Entry会放在同一位置,用链表相连),是通过key的hashCode来计算的。通过hash计算出来的值将会使用indexFor方法找到它应该所在的table下标。
出现死锁原因
refresh核心代码如下:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
//1, 获取旧表的下一个元素
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;
}
}
}
主要逻辑如下:
- 对索引数组中的元素遍历
- 对链表上的每一个节点遍历:用 next 取得要转移那个元素的下一个,将 e 转移到新 Hash 表的头部,使用头插法插入节点。
- 循环直到链表节点全部转移
- 循环直到所有索引数组全部转移
遍历旧数组,将旧数组元素通过头插法的方式,迁移到新数组的。经过这几步会发现转移的时候是逆序的,A->B->C迁移后会变成C->B->A,HashMap 的死锁问题就出在这个transfer()函数上。
例子
我们假设有二个线程T1、T2,HashMap容量为2,T1线程放入key A、B、C、D、E。
在T1线程中A、B、C Hash值相同,于是形成一个链接,假设为A->B->C,而D、E Hash值不同,于是容量不足,需要新建一个更大尺寸的hash表,然后把数据从老的Hash表中迁移到新的Hash表中(refresh)。
这时T2线程进来了,T1执行到代码Entry<K,V> next = e.next线程A挂起,此刻e指向A,next指向B。
T2线程也准备放入新的key,这时也发现容量不足进行refresh。T2开始执行transfer函数中的while循环,会把原来的table变成一个table(线程T2自己的栈中),再写入到内存中。因为转移的时候是逆序的,refresh之后原来的链表结构假设为C->B->A。
T1继续执行执行完一个while循环后,e=B,T1的链表是A。
继续执行T1,此时e=B且B->A,所以next=A,执行一个while循环后,e=A,T1的链表是B->A。
继续执行T1,此时e=A,所以next=null,执行一个while循环后,e=null,线程A的链表是A->B->A
这时就形成A.next=B,B.next=A的环形链表,一旦取值进入这个环形链表就会陷入死循环。
jdk1.8版本相关优化
- 使用尾插法,消除出现循环链表的情况
- 链表过长后,转化为红黑树,提高查询效率