当试图遍历HashMap时,出现了ConcurrentModificationException异常

3
我在处理一个项目时遇到了问题。程序有一个主循环,每帧都会调用 update() 方法。
我有一个名为World的对象,其中包含一个 HashMap<ChunkPos, Chunk>,其中ChunkPos是一个数据结构,它具有xyz位置,代表了ChunkWorld中的位置。
我试图在我的update函数中迭代遍历世界中的所有块,因为每个块都有自己的 GameObject 的数组列表,该列表包含世界中的所有对象。
public void update() {
    HashMap<ChunkPos, Chunk> chunkMap = world.getChunkMap();
    Iterator<Map.Entry<ChunkPos, Chunk>> it = chunkMap.entrySet().iterator();
    while(it.hasNext()) {
        Map.Entry<ChunkPos, Chunk> item = it.next();
        ArrayList<GameObject> chunkObjects = item.getValue().getObjects();
        for(GameObject obj : chunkObjects) {
            obj.update();
        }
    }
}

然而,当我运行程序时,在这行代码处收到了一个ConcurrentModificationException异常:
Map.Entry<ChunkPos, Chunk> item = it.next();

我已经尝试了多种不同的迭代HashMap的方法,但每次都遇到相同的错误。我做错了什么?

如果有帮助的话,以下是完整的StackTrace:

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1429)
at java.util.HashMap$EntryIterator.next(HashMap.java:1463)
at java.util.HashMap$EntryIterator.next(HashMap.java:1461)
at com.anzanama.lwjgl3d.Game.Game.update(Game.java:40)
at com.anzanama.lwjgl3d.Game.Game.loop(Game.java:31)
at com.anzanama.lwjgl3d.Main.main(Main.java:15)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

我想在这里做一个说明:最终我实现了一个WorldChangeScheduler,它将安排在整个世界迭代并更新后进行更改。这使得它更加鲁棒,并且有额外的好处,即我以后可以将这些WorldChanges转换为其他人可以连接到我的引擎的事件。


问题不在于对HashMap的迭代,而是在你迭代时其他事情正在改变这个Map。对其进行同步或类似操作。 - Louis Wasserman
https://dev59.com/-2sz5IYBdhLWcg3wHUO0 - Erwin Bolwidt
鉴于没有提到线程使用,很可能的原因是在调用 obj.update(); 内部修改了 HashMap。除非通过迭代器本身进行操作,否则在迭代集合时不能修改它。 - Erwin Bolwidt
3个回答

4

这里的 ConcurrentModificationException 意味着在迭代过程中未使用迭代器修改了地图。

要么 obj.update(); 修改了 world.getChunkMap() 地图,要么您有另一个线程在当前 void update() 方法迭代期间并发地添加或删除此同一地图的元素。因为在所示代码中没有对地图进行任何修改。


虽然我还没有找到解决问题的方法,但我并没有看到在更新期间chunkMap被修改的地方。此外,程序中只有一个线程,所以这不可能是问题所在。目前,世界中唯一具有实际执行某些操作的update函数的对象是我的PlayerObject,在其updateMovement()函数中最初对chunks进行了更改。然而,我创建了一个“WorldChangeScheduler”,以便我可以安排对世界的更改,然后在迭代哈希映射表完成后进行所有更改。 - Andrew Graber
这是我的代码链接:https://github.com/AndrewGraber/LWJGL3D/blob/master/src/com/anzanama/lwjgl3d/GameObject/PlayerObject.java#L39 - Andrew Graber
没事了,我解决了问题。现在它只是运行得非常慢。我想我可能需要从零重新开始。 - Andrew Graber
很可能。由于存在竞态条件,您必须尽可能减少关键部分的时间(https://en.wikipedia.org/wiki/Critical_section)。另外一个提示是,如果在迭代器循环期间两个客户端之间对Map状态更新的延迟是可以接受的,那么您也可以优先选择ConcurrentHashMap而不是`synchronized`语句/方法。 - davidxxx

1

好吧,这段代码正在引起问题

ArrayList<GameObject> chunkObjects = item.getValue().getObjects();
for(GameObject obj : chunkObjects) {
    obj.update();
}

使用迭代器并调用update方法。
Iterator<GameObject> iter = item.getValue().getObjects().iterator();

while (iter.hasNext()) {
    GameObject obj = iter.next();
    obj.update();
}

0

HashMap 不是线程安全的,因此在迭代时如果其他线程进行结构性修改,则迭代器会快速失败。

通过将您的 Map 实现替换为 ConcurrentHashMap,您将可以避免这些问题,但更新操作将应用锁定,这将影响它们的性能。如果预计有可预测数量的线程同时更新此 Map,则可以使用 ConcurrentHashMap 的 concurrencyLevel 参数来配置并发级别。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接