为什么这段代码会抛出java ConcurrentModificationException异常?

4
public final class ClientGateway {

   private static ClientGateway instance;
   private static List<NetworkClientListener> listeners = Collections.synchronizedList(new ArrayList<NetworkClientListener>());
   private static final Object listenersMutex = new Object();
   protected EventHandler eventHandler;


   private ClientGateway() {
      eventHandler = new EventHandler();
   }

   public static synchronized ClientGateway getInstance() {
      if (instance == null)
         instance = new ClientGateway();
      return instance;
   }

   public void addNetworkListener(NetworkClientListener listener) {
     synchronized (listenersMutex) {
        listeners.add(listener);
     }
   }


   class EventHandler {

     public void onLogin(final boolean isAdviceGiver) {
        new Thread() {
           public void run() {
              synchronized (listenersMutex) {
                 for (NetworkClientListener nl : listeners) 
                    nl.onLogin(isAdviceGiver);
              }
           }
        }.start();
     }

   }
}

这段代码会抛出ConcurrentModificationException异常。但是我认为,如果它们都在listenersMutex上同步,那么它们应该按顺序执行,所有操作listeners列表的函数内部的所有代码都在同步块中运行,这些同步块在Mutex上同步。修改列表的唯一代码是addNetworkListener(...)和removeNetworkListener(...),但目前从未调用removeNetworkListener。
错误似乎是在onLogin函数/线程迭代监听器时仍在添加NetworkClientListener。
感谢您的见解!
编辑:NetworkClientListener是一个接口,并且“onLogin”的实现留给实现函数的编码者,但是他们对函数的实现无法访问listeners List。
另外,我刚刚完全重新检查了代码,除了addNetworkListener()和removeNetworkListener()函数之外,没有修改列表的代码,其他函数只迭代列表。将代码从以下方式更改:
for (NetworkClientListener nl : listeners) 
   nl.onLogin(isAdviceGiver);

致:

for(int i = 0; i < listeners.size(); i++)
   nl.onLogin(isAdviceGiver);

似乎解决了并发问题,但我已经知道这个问题的原因,想知道最初是什么导致了这个问题。感谢您一直以来的帮助!
异常: Exception in thread "Thread-5" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:782) at java.util.ArrayList$Itr.next(ArrayList.java:754) at chapchat.client.networkcommunication.ClientGateway$EventHandler$5.run(ClientGateway.java:283)
编辑:好吧,我感觉有点蠢。但是感谢您所有人的帮助!特别感谢MJB和jprete!
答案:某个人在onLogin()的实现中向网关添加了一个新的监听器。因此(since java's synchronization is based on Threads and is reentrant, so that a Thread may not lock on itself),当调用onLogin()时,在他的实现中,我们正在遍历监听器,并在此过程中,添加了一个新的监听器。
解决方案:使用CopyOnWriteArrayList而不是同步列表(MJB的建议)。

NetworkClientListener#onLogin的代码会很有帮助。 - Affe
商品化在哪里?堆栈跟踪会很有帮助。 - John Vint
只是为了好玩,如果你用 system.out.println(nl) 替换 nl.onLogin,问题是否仍然存在? - MJB
@MJB 嗯,这很奇怪,我已经替换了它并且它可以工作了。我将逐个检查onLogin的每个实现,看看是否有什么东西在执行操作。但是让我困惑的是,每个实现都位于完全不同的包中,它们如何修改“private List listeners”? - random dude
3个回答

4
互斥锁只能保护多线程访问。如果nl.onLogin()中有添加监听器到listeners列表的逻辑,那么可能会抛出ConcurrentModificationException异常,因为它同时被(迭代器)访问和修改(添加)。

编辑:一些更多的信息可能会有所帮助。据我回忆,Java集合通过为每个集合保留修改计数来检查并发修改。每次进行更改集合的操作时,计数都会增加。为了检查操作的完整性,在操作开始和结束时检查计数;如果计数改变,则集合会在访问点而不是在修改点抛出ConcurrentModificationException异常。对于迭代器,它在每次调用next()后检查计数,因此在通过listeners循环的下一个迭代中,您应该看到异常。


我编辑了主贴以注明nl.onLogin并非由我实现,但是它的实现无法访问监听器列表。不过还是感谢您提供的信息! - random dude

3

我必须承认,如果确实没有调用removeListeners,我也看不到它。

nl.onLogin的逻辑是什么?如果它修改了内容,可能会导致异常。

顺便提一下,如果你期望监听器添加得比较少,你可以将列表设置为CopyOnWriteArrayList类型--这样你就不需要使用互斥锁了--CopyOnWriteArrayList是完全线程安全的,并且返回的迭代器是弱一致性的,永远不会抛出CME(除了我刚才说的,在nl.onLogin中)。


谢谢你提供CopyOnWriteArrayList的提示!由于监听器很少被添加/删除,我可能最终会使用它,但是我仍在努力理解为什么我首先会遇到并发异常。(我编辑了主帖子以注明nl.onLogin不是我实现的,但它的实现没有访问listeners列表) - random dude

3

使用线程安全的 CopyOnWriteArrayList 类替代 ArrayList,即使在迭代时被修改也不会抛出 ConcurrentModificationException 异常。当尝试修改(添加、更新)时,它会复制列表,但迭代器将继续在原始列表上工作。

它比 ArrayList 慢一些。它适用于您不想同步迭代的情况。


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