无限循环的原因是结合
- 地图条目如何在内部存储
- 键迭代器如何工作
1
映射条目存储为链表数组:
transient volatile Node<K,V>[] table
每个映射条目都将根据其哈希值(hash % table.length)出现在该数组中的一个链表中:
//simplified pseudocode
public V put(K key, V value) {
int hash = computeHash(key) % table.length
Node<K,V> linkedList = table[hash]
linkedList.add(new Node(key, value))
}
same hash 的 2 个键(如 0 和 4294967297)最终会出现在同一个列表中
2
迭代器的工作非常简单:逐个迭代条目。
鉴于内部存储基本上是集合的集合,它会遍历 table[0] 列表中的所有条目,而不是 table[1] 等等。
但是有一个实现细节使我们的示例永远只针对具有哈希冲突的地图运行:
public final K next() {
Node<K,V> p;
if ((p = next) == null)
throw new NoSuchElementException();
K k = p.key;
lastReturned = p;
advance();
return k;
}
next() 方法实现返回一个之前预先计算的值,并计算在未来调用时要返回的值。当迭代器被实例化时,它收集第一个元素,当next() 第一次被调用时,它收集第二个元素并返回第一个。
以下是advance() 方法的相关代码:
Node<K,V>[] tab; // current table; updated if resized
Node<K,V> next; // the next entry to use
. . .
final Node<K,V> advance() {
Node<K,V> e;
if ((e = next) != null)
e = e.next;
for (;;) {
Node<K,V>[] t; int i, n;
if (e != null)
return next = e; // our example will always return here
. . .
}
}
这是我们地图的内部状态如何演变的:
Map<Long, Long> map = new ConcurrentHashMap<>();
[ null, null, ... , null ]所有桶(链表)都是空的
map.put(0L, 0L);
[ 0:0, null, ... , null ] 第一个桶有一个条目
map.put((1L << 32) + 1, 0L);
[ 0:0 -> 4294967297:0, null, ... , null ] 第一个存储桶现在有两个条目
第一次迭代,迭代器返回0并将4294967297:0条目保存为next
map.remove(0)
[ 4294967297:0, null, ... , null ]
map.put(0, 0) // the entry our iterator holds has its next pointer modified
[ 4294967297:0 -> 0:0, null, ... , null ]
第二次迭代
map.remove(4294967297)
[ 0:0, null, ... , null ]
map.put(4294967297, 0)
[ 0:0 -> 4294967297:0, null, ... , null ]
所以在 2 次迭代后我们又开始了,因为我们的操作归结为从链表的头部删除一个项目并将其添加到它的尾部,因此我们无法完成使用它。
对于没有哈希冲突的映射,它不会陷入无限循环,因为我们添加到的链表已经被迭代器留下了。
这是一个证明它的例子:
Map<Long, Long> map = new ConcurrentHashMap<>();
map.put(0L, 0L);
map.put(1L, 0L);
int iteration = 0;
for (long key : map.keySet()) {
map.put((1L << 32) + 1, 0L);
map.put((1L << 33) + 2, 0L);
map.put((1L << 34) + 4, 0L);
System.out.printf("iteration:%d key:%d map size:%d %n", ++iteration, key, map.size());
map.put(key, map.remove(key));
}
输出为:
iteration:1 key:0 map size:5
iteration:2 key:1 map size:5
在循环中添加的所有项目最终都在同一个存储桶中 - 第一个存储桶 - 我们的迭代器已经使用的存储桶。