比较容易想到的是多线程环境下,如果几个线程同时在一个位置table[i]进行添加或者删除操作,会出现被覆盖或者其它情况。但还有一种比较严重的问题,即在多线程同时操作一个HashMap,进行扩容重排的过程中,有可能会出现环形链表,在下一次进行get操作或者迭代操作时,这里简单地结合JVM解释一下为什么在多线程环境下会出现环形链表。
首先要清楚HashMap扩容时的几个步骤,这里以一个链table[i]为例:
- 循环取出table[i]中的元素e1->e2->null
- 根据indexFor计算这些元素的新的位置,假设这些元素经过indexFor函数计算后聚集在新的newTable[k]这个点上
- 由于插入的顺序是先进入的元素会被后续的元素挤到next的位置,所以新的顺序为e2->e1->null
单线程中这样做没什么问题,但是多线程环境下,每个子线程会存在本地内存(栈)与共享内存(堆)的读写刷新,如果多个线程同时对该表进行扩容,就有可能出现环形链表,下面还是举上例的多线程模式,模拟出出现环链的情况:
- 首先T1和T2两个线程都判断出table需要扩容为newTable,还是假设元素顺序为e1->e2->null,且新表中的indexFor结果为newTable[k]
- T1此时进行循环遍历,关键代码如下:
/**
* 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
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int k = indexFor(e.hash, newCapacity);
e.next = newTable[k];
newTable[k] = e;
e = next;
}
}
}
- 当从table中取出e1,遍历到位置1时,线程T1被挂起,此时线程T1内的Entry<K,V> next = e.next已经被保存,存储的next为本地变量,保存在本地工作内存中,为(e2,指向null)
- 线程T1被挂起后,线程T2完成整个table的扩容过程,此时在共享内存中,newTable已经出现,并且在新的表中,链表顺序变为e2->e1->null,此时newTable[k]为(e2,指向e1)
- 线程T1此时继续执行,e.next此时为newTable[k](e2,指向e1),而存入newTable[k]的元素变为此时本地内存中的变量(e1,指向e2),此时在newTable[k]这个位置,环形链表就已经形成。
在分析HashMap环链形成这个问题中,可以发现,多线程环境下要分析这种冲突问题,需要了解哪些变量是存在栈,哪些是存在堆中,他们之间如何协同,有哪些可能会形成冲突等,所以JVM模型对于此类问题的理解至关重要,在后续的HashTable,ConcurrentHashMap的源码阅读理解中,也需要结合这个模型来考虑多线程环境下为什么不会出现这些冲突,类似的操作中它们是如何规避这种问题的。