【问题标题】:Another ConcurrentModificationException question另一个 ConcurrentModificationException 问题
【发布时间】:2011-05-05 09:49:03
【问题描述】:

我搜索了 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 对象并将其添加到 Registry.proj,而 draw 方法仍在迭代。

我不能很好地用draw方法做一个重试循环,否则它会重新绘制一些对象。所以现在我不得不寻找一个新的解决方案。有没有更稳定的方法来完成我正在做的事情?

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

【问题讨论】:

    标签: iterator arraylist concurrentmodification concurrent-collections


    【解决方案1】:

    当您仍在对集合进行迭代时,您不能直接从集合中删除它,否则您将获得ConcurrentModificationException

    正如您所暗示的,解决方案是在迭代器上调用 remove 方法。这也会将其从基础集合中删除,但它会以迭代器知道发生了什么的方式执行此操作,因此在发现集合已被修改时不会抛出异常。

    【讨论】:

      【解决方案2】:

      顶线,三个建议:

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

      您问题的第 1 部分

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

      由于您可能并不真正需要 List 语义,我建议使用 Collections.newSetFromMap() 将这些 ConcurrentHashMaps 包装到 Set 中。

      您的 draw() 方法可以 a) 使用返回集合快照的 Registry.getEffectsSnapshot() 方法;或 b) 使用返回安全可迭代版本的Iterable&lt;Effect&gt; Registry.getEffects() 方法(可能只是由ConcurrentHashMap 支持,在任何情况下都不会抛出CME)。我认为 (b) 在这里更可取,只要绘制循环不需要修改集合。这在 mutator 线程和 draw() 线程之间提供了非常弱的同步保证,但假设 draw() 线程运行得足够频繁,错过更新或其他什么可能不是什么大问题。

      您问题的第 2 部分

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

      【讨论】:

      • 哇,这是很多有用的信息! (而且可能在我的头上!)我只是从我的牢房里检查 SO.. 当我回到我的比赛时,我还有一些工作要做;)所以我明白你在说什么.. 你说我应该使用私有的、非静态的集合并通过注册表本身的方法访问它们?感谢您的帮助!
      • @Snailer:你明白了......我认为这是最干净的方法,鉴于我们在这里所知道的。而通过“访问”私有集合,我们的意思是提供行为方法,因此Registry 提供了一个真实的数据模型,而不仅仅是一组非结构化集合。
      猜你喜欢
      • 2011-09-11
      • 1970-01-01
      • 1970-01-01
      • 2013-05-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多