【问题标题】:Already synchronized, but got ConcurrentModificationException [duplicate]已经同步,但得到 ConcurrentModificationException [重复]
【发布时间】:2013-10-13 14:09:55
【问题描述】:

这是我的课程,有两种方法修改列表PacketQueue。这两个方法在两个线程中执行,所以标记了synchronize

public class MessageHandler implements nuctrl.interfaces.MessageHandler, Runnable {
    private static final List<GatewayMsg> PacketQueue = new LinkedList<GatewayMsg>();

    @Override
    public void insert(GatewayMsg msg) {
        synchronized (PacketQueue){
            PacketQueue.add(msg);
            PacketQueue.notify();
        }
        log.debug("insert " + msg.toString());
    }

    @Override
    public void run() {
        while(running){
            synchronized (PacketQueue){
                try {
                    while(PacketQueue.size() == 0){
                        PacketQueue.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
                for (GatewayMsg msg : PacketQueue){
                    PacketQueue.remove(msg);
                    packetHandler.onPacket(msg);//method call
                }
            }
        }
    }
}

run() 用于 thread-4insert() 用于另一个线程(I/O Worker #1)。 Synchronized 已添加,一切正常,但我仍然不断收到 ConcurrentModificationException。

DEBUG [New I/O worker #1] (MessageHandler.java:47)| insert GatewayMsg<>
Exception in thread "Thread-4" java.util.ConcurrentModificationException
    at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:761)
    at java.util.LinkedList$ListItr.next(LinkedList.java:696)
    at nuctrl.core.MessageHandler.run(MessageHandler.java:67)
    at java.lang.Thread.run(Thread.java:680)

这让我快疯了!任何人都可以帮助找出错误吗?或者其他方式来做同样的事情?

【问题讨论】:

  • 每次更改后显示列表条目的解决方案和逻辑不是很酷,那么insert()方法中插入后的内容为什么不显示呢?!当列表中有一个(或多个)对象时,它可能会出现错误,因此run() 将永远锁定并显示该条目。
  • 看来您正在尝试解决“生产者-消费者问题”。如果是这样,您可能想查看 java 中的 BlockingQueue,它可以帮助您巧妙地做到这一点。

标签: java multithreading race-condition synchronized


【解决方案1】:

如果同步代码在迭代期间修改了集合,则同步不会阻止ConcurrentModificationException,您在此处执行此操作:

for (GatewayMsg msg : PacketQueue){
    PacketQueue.remove(msg);     // <== Not allowed during iteration
    packetHandler.onPacket(msg);
}

在迭代期间,您只能通过Iterator 删除元素,例如:

Iterator<GatewayMsg> it = PacketQueue.iterator();
while (it.hasNext()) {
    GatewayMsg msg = it.next();
    it.remove();                 // <== This is allowed, provided the collection supports it
    packetHandler.onPacket(msg);
}

【讨论】:

  • 就是这样。很有帮助。我有很多东西要学。谢谢:)
  • @Crowder,您能解释一下for(:)Iterator 之间的区别吗?
  • @ColinZ:enhanced for statement 只是在后台使用迭代器的一种简写方式(有关详细信息,请参阅该链接)。但是使用增强的for 语句,你不能直接使用迭代器,所以你不能访问它的remove 方法,如果你想在迭代过程中删除东西,你需要这个方法。
  • @Crowder 你说的你不能直接使用迭代器意味着PacketQueue.remove(msg)实际上调用了iteratorremove()方法而不是List.remove()
  • @ColinZ:不,PacketQueue.remove()List.remove。我的意思是,如果您使用增强的 for 语句,您将无权访问它使用的底层 Iterator,因此您无法使用该迭代器删除内容(这是删除它们的正确方法) .所以如果你想在循环中删除一些东西,你不能使用增强的for语句;您必须明确使用Iterator,如上所示,这样您就可以访问Iteratorremove
【解决方案2】:

这与同步无关——这样的代码即使在单个线程中也会触发异常:

for (GatewayMsg msg : PacketQueue){
    PacketQueue.remove(msg);
    packetHandler.onPacket(msg);
}

这是因为您正在修改您正在迭代的集合。

要解决此问题,请在循环中使用列表迭代器,并调用迭代器的remove。更好的是,循环处理所有项目,然后一次性清除PacketQueue,如下所示:

for (GatewayMsg msg : PacketQueue){
    packetHandler.onPacket(msg);
}
PacketQueue.clear();

这可以正常工作,因为对PacketQueue 的访问是同步的:在处理了部分消息但仍保留在队列中的状态下,其他线程将看不到PacketQueue

【讨论】:

    【解决方案3】:

    您获得 CME 的原因是您在对其进行迭代时进行修改。库无法区分您的线程和修改它的另一个线程。


    最简单的解决方案是不要自己编写此队列/线程处理代码。我会使用 ExecutorService 编写它

    public class MessageHandler implements nuctrl.interfaces.MessageHandler {
        private static final ExecutorService EXEC = Executors.newSingleThreadExecutor();
    
        @Override
        public void insert(final GatewayMsg msg) {
            EXEC.submit(new Runnable() {
                @Override
                public void run() {
                    packetHandler.onPacket(msg);//method call
                }
            });
            if(log.isDebugEnabled())
                log.debug("submitted " + msg);
        }
    
        public static void stop() {
            EXEC.shutdown();
        }
    }
    

    注意:这不需要包裹在线程中。

    我检查是否启用了日志记录,因为生成不需要的字符串可能会慢得多,通常加速应用程序的最简单方法是避免它们。

    【讨论】:

      猜你喜欢
      • 2013-03-04
      • 2021-08-15
      • 1970-01-01
      • 1970-01-01
      • 2023-03-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-26
      相关资源
      最近更新 更多