【问题标题】:Threads and Concurrent Modification Exception working with a list使用列表的线程和并发修改异常
【发布时间】:2012-09-26 00:18:52
【问题描述】:

我知道这是一个愚蠢的问题,但不知道如何解决这个问题,我以前没有太多使用线程的经验。

下面应该首先创建一个计时器,它将每 10 秒执行一次命令 output.write(mylist),它将简单地输出 mylist 的内容。

其次,它循环遍历我拥有的大约 3 个列表,并为每个列表创建一个线程,该线程将继续循环获取列表中的下一个单词。 请注意:这是精简版,并不完整,因此请不要评论数组列表/列表,而是评论错误本身。

当它尝试执行output.write() 时,经常发生并发修改异常,但并非一直发生。我猜这是因为其他线程之一当前正在将某些内容保存到mylist?我该如何解决这个问题?

    Runnable timer = new Runnable() {
        public void run() {
            try {
                while (true) {
                    Thread.sleep(10000);
                    output.write(mylist);
                }
            } catch (InterruptedException iex) {}
        }
    };
    Thread timerThread = new Thread(timer);
    timerThread.start();

    for (final ValueList item : list) {

        Runnable r = new Runnable() {
            public void run() {
                try {
                    while (true) {

                        char curr = item.getNext();

                         mylist.addWord(curr);
                    }
                } catch (InterruptedException iex) {}
            }
        };

        Thread thr = new Thread(r);
        thr.start();
    }
}

【问题讨论】:

  • output.write(mylist); 是做什么的?我认为您缺少那里的同步步骤。考虑发布代码的相关部分,如果不是 SSCCE 突出您的问题。

标签: java multithreading exception concurrency


【解决方案1】:

当它试图做output.write...时,经常发生并发修改异常,但不是一直发生。

问题是(我假设)output.write(...) 方法正在迭代您的myList,同时另一个线程正在调用myList.addWord(curr);。除非myList 是并发集合,否则这是不允许的。

我将如何解决这个问题?

您需要在每次访问myList 时进行同步——在这种情况下,当您输出它或添加一个单词时。

  synchronized (myList) {
      output.write(mylist);
  }
  ...

  synchronized (myList) {
      myList.addWord(curr);
 }

在这种情况下,因为output.write(mylist) 可能正在遍历列表,所以不能使用Collections.synchronizedList(...) 方法,因为调用者需要同步迭代器。

如果这是一种被多次调用的高性能方法,那么您也可以使用ConcurrentLinkedQueue,但显然这是一个队列,而不是一个列表。 ConcurrentSkipList 是另一种选择,尽管它是一种更重的数据结构。

【讨论】:

  • 我同意这个解决方案,解释得很好。
  • -1 不正确: “您还可以将 myList 定义为同步列表,在这种情况下,Collections 包装类将为您处理同步。”请参阅hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/…,搜索“必须由用户手动同步!”,另请参阅此答案的底部:stackoverflow.com/a/38199451/3500521 关于复合操作。
  • 你完全正确@DreamspacePresident。我错过了那里的隐含迭代器。谢谢!
  • :) 删除了 -1,奇怪的是仍然可能,可能是因为您的编辑。
【解决方案2】:

就像你说的那样,发生这种情况是因为 Java 迭代器是快速失败的,如果同时修改集合会失败。 一种简单的解决方案是使用synchronized 保护对您的列表的访问,但这会导致您的线程停止,等待输出完成。

一个可能更聪明的解决方案是使用 List 的一些并发实现。 由于您的列表经常被修改,CopyOnWriteArrayList 不是您想要的解决方案。 如果不需要太多修改,您可以使用由链表支持的ConcurrentLinkedDequeLinkedBlockingDeque,并且 实现List,因此它们不会'没有get 方法(您似乎没有使用)。无论如何,这些集合返回的迭代器不是快速失败的,而是弱一致的(这意味着它们将“看到”集合在创建迭代器时的样子,或者反映后来的一些修改)。

另一种解决方案可能是使用并发集合 (ConcurrentLinkedDeque) 和 ReadWriteLock,但以更原始的方式。您的编写器线程将使用读锁(因此能够同时写入)。您的打印机线程将获取写锁(从而暂时阻塞其他线程),将集合的副本复制到非并发集合中,释放写锁并最终打印其本地副本。

一个绝对更聪明(可能更快)的选择是为每个线程分配一个不同的非并发列表。所以你会有一个非并发列表的列表,每个列表都分配给一个线程。完成所有线程后,您可以将所有列表合并到一个列表中。此外,每个列表都应该受到锁的保护。当您的打印机线程想要打印时,它会遍历列表,一次锁定一个,复制一份,释放锁定,然后打印。

我坚持制作副本的原因是它比打印 I/O 快得多。如果您的工作线程必须等待打印机线程打印列表,您的所有计算都会变慢。

PS:即使你能看到的唯一错误是异常,ArrayLists 也不是线程安全的。如果您从多个线程将元素添加到同一个列表中,您将丢失一些元素(除非发生一些更奇怪的异常)。

【讨论】:

    【解决方案3】:

    或者只是在run()的减速度上加上synchronized。或者将方法的内容包含在synchronized{...} 块中,只要睡眠/等待位于“同步区域”中,即一次只允许一个线程操作。

    任何阅读此内容的人都应该阅读以下内容:
    http://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html
    这将是对时间的合理投资;)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-11-19
      • 1970-01-01
      • 2012-03-21
      • 1970-01-01
      • 2013-03-12
      • 1970-01-01
      相关资源
      最近更新 更多