这是一个老生常谈的内容了,最近复习发现博客中居然没有记录这块,今天特地记录下。
HashMap 本身是线程不安全的,如果线程并行插入元素,可能会同时触发扩容。这里会新建一个更大的数组,并调用 transfer 方法对元素进行转移,转移的逻辑也很很好理解,就是遍历原来 table 中每个位置的节点,并对每个元素进行重新 hash,在新的 newTable 找到位置,并插入。
transfer 方法如下:
/**
* 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;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//最开始newTable[i];是null
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
就是说里面有一个类似链表倒排的过程:
也就是在原来数组中是一个table里面是,k1,k2,k3,在新数组中就是 k3,k2,k1,使用的头插法:
正常来的扩容(便于理解和模拟,假定新的元素 rehash 后都在同一个数组位置):
之前的结构:
扩容后:
也就是倒过来了。
接下来分析链表出现环的过程。这里说一种情况,比如线程二走到了这一步:
然后线程一开始执行 transfer 方法,并且执行完成,那么此时的内存分配为:
注意线程二的 e=k1,next=k2,接下来线程二执行第一个循环:
第一个循环最后是 e=next=k2。
线程二再执行第二个循环,要注意此时 e=k2,next = e.next = k1:
线程二执行第二个循环:
目前看着没啥问题,第二个循环走到最后一步就是 e=next=k1,接下来线程二再走一次循环,此时要处理 k1。那么就会出现:
即出现了循环链表。
最终线程一或者线程二总有一个会将自己的 newTable 进行替换。
首先无论最终是线程一还是线程二的 newTable 进行了替换。假如我现在要进行 get 操作,刚好是到我们处理的那个数组位置。就会出现遍历环形链表,就会出现了死循环。
假如是线程二的 newTable 替换了,那 k3 数据就会被丢失。