即使在使用Collections.synchronizedMap对LinkedHashMap进行同步处理时,仍然会出现ConcurrentModificationException异常。

5

我在我的类中使用了一个Map对象,并且用Collections.synchronizedMap()方法实现了与LinkedHashMap同步:

private GameObjectManager(){
        gameObjects = Collections.synchronizedMap(new LinkedHashMap<String, GameObject>());
}

我在这个函数的第三行遇到了并发修改异常:

public static void frameElapsed(float msElapsed){
    if(!INSTANCE.gameObjects.isEmpty()){
        synchronized(INSTANCE.gameObjects){
            for(GameObject object : INSTANCE.gameObjects.values()){...}
        }
    }
}

我在遍历Map的所有其他位置上,都按照文档要求在Map上进行同步。

我的类中还有其他函数使用了这个Map(同步的那个!)并且会put()和remove()对象,但这应该无关紧要。我做错了什么?请要求更多的代码,不确定还需要什么。

哦,还有日志信息:

08-20 15:55:30.109: E/AndroidRuntime(14482): FATAL EXCEPTION: GLThread 1748
08-20 15:55:30.109: E/AndroidRuntime(14482): java.util.ConcurrentModificationException
08-20 15:55:30.109: E/AndroidRuntime(14482):    at     java.util.LinkedHashMap$LinkedHashIterator.nextEntry(LinkedHashMap.java:350)
08-20 15:55:30.109: E/AndroidRuntime(14482):    at     java.util.LinkedHashMap$ValueIterator.next(LinkedHashMap.java:374)
08-20 15:55:30.109: E/AndroidRuntime(14482):    at     package.GameObjectManager.frameElapsed(GameObjectManager.java:247)
08-20 15:55:30.109: E/AndroidRuntime(14482):    at     package.GamekitInterface.render(Native Method)
08-20 15:55:30.109: E/AndroidRuntime(14482):    at     package.GamekitInterface.renderFrame(GamekitInterface.java:332)
08-20 15:55:30.109: E/AndroidRuntime(14482):    at     com.qualcomm.QCARSamples.ImageTargets.GameEngineInterface.onDrawFrame(GameEngineInterface.java:107)
08-20 15:55:30.109: E/AndroidRuntime(14482):    at     android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1516)
08-20 15:55:30.109: E/AndroidRuntime(14482):    at     android.opengl.GLSurfaceView$GLThread.run(GLSurfaceView.java:1240)

无论使用哪个函数,您都应该使用gameObjects。如果您调用两次GameObjectManager()函数,则第一个gameObjects和第二个gameObjects不是同一个对象,因此可能会导致ConcurrentModificationException异常。 - 王奕然
我不明白你说的是什么。但我注意到在同步之后应该进行isEmpty测试。这就是你说的吗? - mpellegr
3个回答

12

尽管它的名字听起来像是多线程中的并发,但事实上并没有任何关系。在迭代此地图时,除了通过调用迭代器上的remove()之外,您不能修改它。也就是说,当您具有...

for(GameObject object : INSTANCE.gameObjects.values()){...}

如果...修改了INSTANCE.gameObjects.values()(例如,删除或添加元素),那么在迭代器上调用next()的下一次调用(这是隐含在for循环中的)会抛出异常。

这适用于大多数集合和Map实现。尽管不总是显而易见,但javadoc通常会指定该行为。

修复方法:

  • 如果您要删除元素,请显式获取Iterator<GameObject>并在其上调用remove()

for (Iterator<GameObject> iter = INSTANCE.getObjects().values(); iter.hasNext(); ;) {
     GameObject object = iter.next();
     if (someCondition(object)) {
         iter.remove();
     }
 }
  • 如果您想添加一个元素,您需要创建一个临时集合来保存要添加的元素,然后在迭代器完成之后,putAll(temporaryMapForAdding)

  • 我在任何迭代中都不会添加或删除任何内容。我在从另一个线程调用的其他函数中进行添加和删除。这样做不允许吗? - mpellegr
    1
    是的,这就是“synchronized”块的整个意义。在“synchronized”块完成之前,其他线程将无法调用地图上的任何方法。但这并不会导致此问题;即使只有一个线程,这个问题也会存在。 - yshavit
    啊,我明白发生了什么!通过一长串的函数调用,在迭代过程中对象被添加到了地图中。我会将此标记为答案,但您知道我是否可以使用已经存在的逻辑来添加/删除地图中的对象,而不必担心我是否正在进行迭代吗? - mpellegr
    很遗憾,实际上没有一个好的方法。这种复杂性是不可变对象受欢迎的原因之一。 - yshavit
    真遗憾。我可能能够复制地图并迭代可能过时的值,但至少我不必重构大量代码。感谢您的帮助! - mpellegr

    2
    Collections.synchronizedMap() 在迭代时无法帮助您。它只会使您的 map 执行 put/get/remove 操作原子化(这意味着您不会同时运行两个这样的操作)。
    在迭代时,您会获取每个元素并对其执行某些操作。但是,如果其他线程删除了您正在迭代的当前元素,则会怎样?
    这就是此异常试图防止的情况,因为您可能会得到与您的 map 的任何实际快照不对应的结果:例如,如果您正在计算 Integer 值的总和,则已添加的元素可能会被删除,而在您迭代时可能会添加其他元素,因此您最终将得到一个与您的 map 任何“快照”都不匹配的总和。
    对于您要做的事情,唯一的解决方案是在某个同步块中执行整个迭代,但是必须在与您的 map 操作使用的相同监视器上同步。而 Collections.syncrhonizedMap() 提供了一个包装器,该包装器在某些内部互斥体上进行同步,而不是在 this 引用上进行同步。因此,在迭代时尝试防止对 map 进行任何修改的尝试将失败。

    我将所有在地图上迭代的实例放在同步块中。 - mpellegr
    我明白了...我完全忽略了那个; 对不起!我会编辑我的答案给你一个提示。 - Costi Ciudatu
    内部互斥锁在默认情况下实际上是在构造函数中提供的“this”。 - Joonas Vali

    2

    您正在使用类似于for循环的for-each版本。在Java中,禁止在此类循环中添加或删除迭代集合中的元素。为避免这种情况,请使用集合迭代器。从迭代器中,您可以删除元素。


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