HashMap闭合环路
很有可能就是在两个线程在这个时候同时触发了rehash操作,产生了闭合的回路。下面我们从源码中一步一步地分析这种回路是如何产生的。先看一下put操作:
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //存在key,则替换掉旧的value for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //table[i]为空,这时直接生成一个新的entry放在table[i]上 addEntry(hash, key, value, i); return null; }
addEntry操作:
void addEntry(int hash, K key, V value, int bucketIndex) { ry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) resize(2 * table.length); }
可以看到,如果现在size已经超过了threshold,那么就要进行resize操作:
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; //将旧的Entry数组的数据转移到新的Entry数组上 transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }
看一下transfer操作,闭合的回路就是在这里产生的:
void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; /* * 在转换的过程中,HashMap相当于是把原来链表上元素的的顺序颠倒了。 * 比如说 原来某一个Entry[i]上链表的顺序是e1->e2->null,那么经过操作之后 * 就变成了e2->e1->null */ for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { //我认为此处是出现死循环的罪魁祸首 Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
那么回路究竟是如何产生的呢,问题就出在next=e.next这个地方,在多线程并发的环境下,为了便于分析,我们假设就两个线程P1,P2。src[i]的链表顺序是e1->e2->null。我们分别线程P1,P2的执行情况。
首先,P1,和P2进入到了for循环中,这时候在线程p1和p2中,局部变量分别如下:

此时两个Entry的顺序是依然是最开始的状态e1->e2->null, 但是此时p1可能某些原因线程暂停了,p2则继续执行,并执行完了do while循环。这时候Entry的顺序就变成了e2->e1->null。在等到P2执行完之后,可能p1才继续执行,这时候在P1线程中局部变量e的值为e1,next的值为e2(注意此时两个元素在内存中的顺序变成了e2->e1->null),下面P1线程进入了do while循环。这时候P1线程在新的Entry数组中找到e1的位置,
e.next = newTable[i]; newTable[i] = e;
下面会把next赋值给e,这时候e的值成为了e2,继续下一次循环,这时候

e2->next=e1,这个是线程P2的"功劳"。程序执行完这次循环之后,e=e1,
继续第三次循环,这时候根据算法,就会进行e1->next=e2。
这样在线程P1中执行了 e1->next=e2,在线程P2中执行了 e2->next=e1,这样就形成了一个环。在get操作的时候,next值永远不为null,造成了死循环。
实际上,刚开始我碰到这个说法的时候,还被吓了一跳,HashMap怎么还会出现这个问题呢,仔细分析一下,这个问题再高并发的场景下是很容易出现的。Sun的工程师建议在这样的场景下应采用ConcurrentHashMap。具体参考http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457 。
参考:
https://www.cnblogs.com/vinozly/p/5185191.html
浙公网安备 33010602011771号