JDK1.7中多线程扩容导致的循环链表
首先我们先来看一组单线程下扩容的例子:
单线程扩容(引用木霖森77大神博客):
假设:hash算法就是简单的key与length(数组长度)求余。
hash表长度为2,如果不扩容, 那么元素key为3,5,7按照计算(key%table.length)的话都应该碰撞到table[1]上
扩容:hash表长度会扩容为4
重新hash,key=3 会落到table[3]上(3%4=3), 当前e.next为key(7), 继续while循环
重新hash,key=7 会落到table[3]上(7%4=3), 产生碰撞, 这里采用的是头插入法,所以key=7的Entry会排在key=3前面(这里可以具体看while语句中代码)
当前e.next为key(5), 继续while循环
重新hash,key=5 会落到table[1]上(5%4=3), 当前e.next为null, 跳出while循环, resize结束
多线程扩容问题
我们截取jdk1.7的一部分代码
/**
* Transfers all entries from current table to newTable.
*/
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;//线程1在这句话执行完成后挂起,cpu交给线程2进行处理
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;
}
}
}
-
假如我们两个线程同时进行put操作,而线程1在执行完while循环的第一句话后,线程1挂起,然后由线程2 进行扩容处理。
- 线程2扩容完成之后如上图这样
- 然后现在开始执行线程1后面的代码,因为当时线程1已经对e和next已经赋完值了,所以现在的e和next就是这样的指向关系,如下图
- 现在开始执行后面的代码
- 第一次循环 e -> (key=3) next->(key=7)
- ①第一步:int i = indexFor(e.hash, newCapacity); 先计算e指向的(key = 3)的这个节点的需要在新的表中的位置 ,简单点计算就使用key%table.length取模来确定下标位置
- ②第二步:e.next = newTable[i]; (这里需要注意这个newTable[i]是线程1的table表,线程1的table表是刚创建出来的,所以数组里面的东西为null,这个不是线程2的table表,在研究这个时候曾经我就出现了这种错误,当成了线程2的table来进行处理了),
- 也就是给 (key=3) 的next指针指向了null,上面的图我已经画出了。
- ③第三步:newTable[i] = e; 这一步又将(key=3)的这个节点赋值给了线程1的table表,那么此时的图应该是这样的
- ④第四步:e = next; 将上面执行next的指针赋值给e,也就是e指向了(key=7)的节点。
- 第一次循环 e -> (key=3) next->(key=7)
- 第二次循环 e->(key = 7)
- ①第一步:Entry<K,V> next = e.next; 此时的e是指向7这个节点的,那么他的next节点接是(key = 3)
- ②第二步:int i = indexFor(e.hash, newCapacity); 重新计算7这个节点的下标位置,key % table.length ,那么计算出来的就是3
- ③第三步:e.next = newTable[i]; 这个newTable[i] 就是线程1的table[3] 的这个位置,将(key=7)指向(key=3)这个节点
- ④第四步:newTable[i] = e; 将线程1的table 表的第三个位置指向e也就是指向(key = 7)的这个位置
- 那么此时指向关系应该是这样的
- 整理的话就是这样的
- ⑤第五步:e = next; 此时将(key=3)的节点重新赋值给e
- 第三次循环:
- ①第一步:Entry<K,V> next = e.next; 现在e指向了3 所以next = e.next = null
- ②第二步:int i = indexFor(e.hash, newCapacity); 重新计算下标位置
- ③第三步:e.next = newTable[i]; 将线程1的table[3]的位置重新赋值为(key = 3)的next
- 那么现在图应该是这样的
- ④第四步:newTable[i] = e; 将e作为链表的头指针,那么此时图应该是这样的
- 整理下如图
- 同时(key=7)的next指针同时又指向了(key = 3)这样就形成了链式循环
- ⑤第五步:e = next; 将null赋值给e,此时跳出循环,此时链式不会引发什么问题,但是当我们get(3)的时候这样就出现了get死循环