回顾
在JDK7中,HashMap::transfer在多线程情况下会产生死循环。而在JDK8中,HashMap做了较大的改版。transfer中的死锁被解决。而且Table的存储结构由7中的数组+链表改为了数组+链表/红黑树。
在JDK8中,HashMap会优先使用数组+链表存储。当Hash冲突达到8个时,会将整个Table转为数组+红黑树的形式来存储,以此提高Hash冲突时的查询效率。
JDK7下的死循环问题
HashMap 死循环主要发生在扩容时。发生死锁的核心代码如下,为方便分析省略了部分代码,在重要代码上标记了行数,方便后面分析。
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; // 语句2
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; // 语句3
newTable[i] = e; // 语句4
e = next; // 语句5
}
}
}
先说结论。死循环发生后,CPU飙升,而内存中的数据可能如下:
第一次循环,开始! e 指向 a 元素,next指向a.next。注意,此时a.next仍然为b,因为线程1的修改对线程2不可见。因此,next指向b。在语句5执行后内存中数据如下
JDK8的改进
在JDK8中,对7中的死循环做了改进,我对核心代码做了精简,方便理解
private Node<K, V>[] resize() {
// .... some operation
Node<K,V> loHead = null, loTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// .... some operation
return newTab;
}
7 中死锁的本质原因是由于对链表的倒序重排,JDK8中取消了这种操作。因此在多线程情况下,这段代码造成的最坏影响是丢失数据,而不再是死循环了。
JDK8下的死循环问题
即使JDK8对HashMap做了优化,但是在多线程下还是会产生死循环。参考了HashMap在1.8中也会死循环的测试代码和堆栈信息,我们来开始分析。 死循环发生在HashMap::TreeNode::root方法中,其代码如下
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
我们可大致推出其内存中的数据结构应该如下
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) { // 语句1
if ((lr = p.left = l.right) != null) // 语句2
lr.parent = p; // 语句3
if ((pp = l.parent = p.parent) == null) // 语句4
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p; 语句5
p.parent = l;
}
return root;
}
这段代码处理了四种场景下的子树右旋转,详情如下,这里我们先忽略变色,专注于数据的变换。
此时,同样当两个线程同时开始右旋转时,假设均为场景2。 内存中数据此时为(未标注的均假设为null)
扩展
JDK8中对于数据寻址做了简化,且在resize时位运算做的非常巧妙,非常值得一看。
左旋转核心代码如下
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
这段代码处理了四种情况下的子树左旋转,详情如下