53:从源码看jdk1.8之前的HashMap为什么会产生死锁

121 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


众所周知HashMap不是一个线程安全的Map,并发情况下可能会出现数据丢失,除此之外,JDK1.8之前的HashMap在并发场景下还可能产生死锁。

JDK1.8之前为什么会产生死锁:

想要了解产生死锁的原因是首先要了解JDK1.8之前的HashMap扩容原理,因为死锁正是由多个线程同时进行扩容操作导致的,HashMap底层容器为一个Entry[]数组,扩容就是建立一个目标容量的新数组,并将元素重新放到新数组内,并将新数组赋值为容器即完成了扩容,产生死锁的关键就在于从旧数组转移元素到新数组,JDK1.8之前这个过程通过transfer方法实现:

// newTable即为新容器,rehash为是否需要重新计算hash值,无需关注
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; // 记录next
            if (rehash) { // 必要时重新计算hash
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity); // 算出新的下标
            e.next = newTable[i]; // 注意!!! 将新容器的i处节点赋值到当前元素的next上
            newTable[i] = e; // 当前元素成为新的头节点
            e = next; // 继续下一个节点
        }
    }
}

JDK1.8之前,HashMap底层是通过数组与链表实现的,JDK1.8才引入红黑树的概念。因此JDK1.7转移源码只针对链表进行操作了:

关键在于e.next = newTable[i];newTable[i] = e;e = next;这最后三行源码。这三行源码会使得引用关系逆序:假设扩容之前i处有元素A->B->C,扩容之后A,B,C三个元素依然在i处。此时遍历A->B->C的过程为:

HashMap迁移过程

可以看到迁移之后的引用关系产生的逆转,那么如果线程T1进行put操作,发现容量不足执行扩容操作,在未扩容时T2也进行put操作,发现容量不足同时进入扩容流程,是不是会发生这种情况呢:

HashMap死锁产生过程

至此死锁产生....

JDK1.8的优化:

JDK1.8 HashMap除了引入了红黑树以加快检索速度外,对于扩容时链表元素的转移也进行了优化避免了死锁问题:

链表元素转移关键代码:

do {
	next = e.next;
	if ((e.hash & oldCap) == 0) {	// 为0说明这个元素在原位,即为低位
		if (loTail == null)
			loHead = e;	// 记录低位抬头
		else
			loTail.next = e;	// 链接到loHead 所在的链表上
		loTail = e;	// 记录尾部
	}
	else {	// 不为为0说明这个元素在j + oldCap,即为高位
		if (hiTail == null)
			hiHead = e;	// 记录高位抬头
		else
			hiTail.next = e;// 链接到hiTail所在的链表上
		hiTail = e;	// 记录尾部
	}
} while ((e = next) != null);
if (loTail != null) {	// loTail 不为空说明低位有值
	loTail.next = null;	// 断开loTail.next的链接,因为loTail为尾部,若是loTail.next不为空,那么他会在高位
	newTab[j] = loHead;	// 赋值到新容器的对应位置
}
if (hiTail != null) {	// hiTail 不为空说明高位有值
	hiTail.next = null; // 断开hiTail.next的链接,因为hiTail为尾部,若是hiTail.next不为空,那么他会在低位
	newTab[j + oldCap] = hiHead;// 赋值到新容器的对应位置
}

JDK1.8通过记录头尾双节点的方式,保证了同位元素引用顺序保持不变,以此避免了元素引用逆序所导致的并发死锁问题。


开发成长之旅 [持续更新中...]
欢迎关注…