关于SO的一些答案提到HashMap中的get方法可能会落入无限循环(例如this one或this one),如果没有正确同步(通常底线为&#34) ;不要在多线程环境中使用HashMap,使用ConcurrentHashMap")。
虽然我可以很容易地看到为什么对HashMap.put(Object)方法的并发调用会导致无限循环,但我不能完全理解为什么get(Object)方法在尝试读取时会卡住正在调整大小的HashMap。我查看了implementation in openjdk并且它包含一个循环,但退出条件e != null
迟早应该完成。它怎么能永远循环?
明确提到易受此问题影响的一段代码是:
public class MyCache {
private Map<String,Object> map = new HashMap<String,Object>();
public synchronized void put(String key, Object value){
map.put(key,value);
}
public Object get(String key){
// can cause in an infinite loop in some JDKs!!
return map.get(key);
}
}
有人可以解释一个线程如何将一个对象放入HashMap,另一个读取它是否可以交错以产生无限循环?它是否与某些缓存一致性问题或CPU指令重新排序有关(所以问题只能在多处理器机器上发生)?
答案 0 :(得分:11)
你链接用于Java 6中的HashMap。它在Java 8中重写。在重写之前,如果有两个写入线程,则get(Object)
上的无限循环是可能的。我不知道单个编写器可以在get
上发生无限循环的方式。
具体来说,当有两个同时调用resize(int)
来调用transfer
时会发生无限循环:
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);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
此逻辑反转了哈希桶中节点的顺序。两个同时反转可以形成循环。
看看:
e.next = newTable[i];
newTable[i] = e;
如果两个线程正在处理同一个节点e
,则第一个线程正常执行,但第二个线程设置e.next = e
,因为newTable[i]
已经设置为e
第一个主题。节点e
现在指向自身,当调用get(Object)
时,它进入无限循环。
在Java 8中,resize维护节点排序,因此不能以这种方式发生循环。你可以丢失数据。
当有多个读取器时,LinkedHashMap
类的迭代器可能陷入无限循环,而在维护访问顺序时没有写入器。使用多个读取器和访问顺序,每个读取都会删除,然后从双链接的节点列表中插入所访问的节点。多个读取器可能导致同一节点多次重新插入到列表中,从而导致循环。该类已经重写为Java 8,我不知道这个问题是否仍然存在。
答案 1 :(得分:3)
<强>情况:强>
HashMap的默认容量为16,加载因子为0.75,这意味着当第12个键值对在地图中输入时,HashMap的容量会翻倍(16 * 0.75 = 12)。
当2个线程同时尝试访问HashMap时,您可能会遇到无限循环。线程1和线程2尝试放置第12个键值对。
线程1获得执行机会:
线程1指向键值对之后,在开始传输过程之前,松开控件,线程2有机会执行。
线程2获得执行机会:
线程1获得执行机会:
<强>解决方案:强>
要解决此问题,请使用Collections.synchronizedMap
或ConcurrentHashMap
。
ConcurrentHashMap 是线程安全的,即一次只能由单个线程访问代码。
可以使用Collections.synchronizedMap(hashMap)
方法同步HashMap。通过使用此方法,我们得到一个HashMap对象,它等同于HashTable对象。所以每次修改都是在地图上锁定的地图对象上执行的。
答案 2 :(得分:1)
鉴于e.next = e
方法中我看到无限循环的唯一可能性是get
:
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next)
这只能在调整大小期间transfer
方法中发生:
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; //here e.next could point on e if the table is modified by another thread
newTable[i] = e;
e = next;
} while (e != null);
如果只有一个线程正在修改Map,我相信只用一个线程就不可能有一个无限循环。在jdk 6(或5)之前使用get
的旧实现更为明显:
public Object get(Object key) {
Object k = maskNull(key);
int hash = hash(k);
int i = indexFor(hash, table.length);
Entry e = table[i];
while (true) {
if (e == null)
return e;
if (e.hash == hash && eq(k, e.key))
return e.value;
e = e.next;
}
}
即便如此,除非发生很多碰撞,否则案件似乎仍然不太可能。
P.S:我很想被证明是错的!