另一个ConcurrentModificationException问题

时间:2010-11-05 01:18:18

标签: iterator arraylist concurrentmodification concurrent-collections

我搜索过StackOverflow,并且有很多ConcurrentModificationException问题。读完之后,我还是很困惑。我得到了很多这些例外。我正在使用“注册表”设置来跟踪对象:

public class Registry {
    public static ArrayList<Messages> messages = new ArrayList<Messages>();
    public static ArrayList<Effect> effects = new ArrayList<Effect>();
    public static ArrayList<Projectile> proj = new ArrayList<Projectile>();

    /** Clears all arrays */
    public static void recycle(){
        messages.clear();
        effects.clear();
        proj.clear();
    }
}

我通过访问这样的ArrayLists来添加和删除这些列表中的对象:Registry.effects.add(obj)Registry.effects.remove(obj)

我设法通过使用重试循环来解决一些错误:

//somewhere in my game..
boolean retry = true;
while (retry){
    try {
        removeEffectsWithSource("CHARGE");
        retry = false;
    }
catch (ConcurrentModificationException c){}
}

private void removeEffectsWithSource(String src) throws ConcurrentModificationException {
    ListIterator<Effect> it = Registry.effects.listIterator();
    while ( it.hasNext() ){
        Effect f = it.next();
        if ( f.Source.equals(src) ) {
            f.unapplyEffects();
            Registry.effects.remove(f);
        }
    }
}

但在其他情况下,这是不切实际的。我一直在我的drawProjectiles()方法中获取ConcurrentModificationExceptions,即使它没有修改任何内容。我想罪魁祸首就是如果我触摸屏幕,它会创建一个新的Projectile对象,并在draw方法仍在迭代时将其添加到Registry.proj。

我不能很好地使用draw方法进行重试循环,否则它会重新绘制一些对象。所以现在我被迫找到一个新的解决方案..是否有一种更稳定的方式来完成我正在做的事情?

哦,我的问题的第2部分:许多人建议使用ListIterators(我一直在使用),但我不明白..如果我调用ListIterator.remove()它是否从ArrayList中删除该对象它正在迭代通过,或者只是从迭代器本身中删除它?

2 个答案:

答案 0 :(得分:2)

顶线,三条建议:

  • 不要做“在循环中包装异常”的事情。例外情况是特殊条件,而不是控制流程。 (Effective Java#57或Exceptions and Control Flow"using exceptions for control flow"的示例)
  • 如果您要使用Registry对象,请在该对象上公开线程安全的行为,而不是 accessor 方法,并在该单个类中包含并发推理。你的生活会变得更好。 公开字段中没有公开集合。 (呃,为什么这些字段static?)
  • 要解决实际的并发问题,请执行以下操作之一:
    1. 使用同步集合(潜在的性能影响)
    2. 使用并发集合(有时是复杂的逻辑,但可能很有效)
    3. 使用快照(可能包含synchronizedReadWriteLock

问题的第1部分

您应该为多线程方案使用并发数据结构,或者使用同步器并制作防御性副本。可能直接将集合公开为public字段是错误的:您的注册表应该向这些集合公开线程安全的行为访问器。例如,您可能需要Registry.safeRemoveEffectBySource(String src)方法。保持注册表内部的线程细节,这似乎是您设计中此聚合信息的“所有者”。

由于您可能并不真正需要List语义,因此我建议使用ConcurrentHashMapsCollections.newSetFromMap()替换为Set替换它们。

您的draw()方法可以a)使用返回集合快照的Registry.getEffectsSnapshot()方法;或者b)使用返回安全可迭代版本的Iterable<Effect> Registry.getEffects()方法(可能仅由ConcurrentHashMap支持,在任何情况下都不会抛出CME。我认为(b)在这里是优选的,只要绘制循环不需要修改集合。这在mutator线程和draw()线程之间提供了非常弱的同步保证,但假设draw()线程经常运行,缺少更新或某些事情可能不是什么大问题。< / p>

问题的第2部分

正如另一个答案所指出的,在单线程的情况下,你应该确保使用Iterator.remove()来删除项目,但是再次,你应该将这个逻辑包装在Registry类中。在所有可能的。在某些情况下,您需要锁定一个集合,迭代它以收集一些聚合信息,并在迭代完成后进行结构修改。您询问remove()方法是否只是从Iterator或后备集合中删除它...请参阅API contract for Iterator.remove(),它告诉您它从基础集合中删除了对象。另请参阅此SO question

答案 1 :(得分:1)

当您仍然在迭代时,您无法直接从集合中删除某个项目,否则您将获得ConcurrentModificationException

正如您所暗示的那样,解决方案是在Iterator上调用remove方法。这也将从底层集合中删除它,但是它将以Iterator知道发生了什么的方式执行它,因此当它发现集合被修改时不会抛出异常。