尝试运行`.size()`方法时,ArrayList抛出`ConcurrentModificationException`

时间:2019-05-22 15:11:14

标签: java

更新

正如Jiri Tousek指出的那样,我的代码中抛出的错误误导了许多业余(和经验丰富)的Java开发人员。与名称所隐含的含义相反,ConcurrentModificationException 不是 无关。考虑以下代码:

import java.util.List;
import java.util.ArrayList;

class Main {
  public static void main(String[] args) {
    List<String> originalArray = new ArrayList<>();
    originalArray.add("foo");
    originalArray.add("bar");
    List<String> arraySlice = originalArray.subList(0, 1);
    originalArray.remove(0);
    System.out.println(Integer.toString(arraySlice.size()));
  }
}

尽管不涉及线程,但这仍将抛出ConcurrentModificationException

误导性的异常名称使我认为我的问题是我处理多线程的结果。我已经用 actual 问题更新了帖子的标题。

原始(标题:如何通知Java您已完成对线程中ArrayList的修改?)

我的代码大致如下:

class MessageQueue {
    private List<String> messages = new ArrayList<>();
    private List<String> messagesInFlight = new ArrayList<>();

    public void add(String message) {
        messages.add(message);
    }

    public void send() {
        if (messagesInFlight.size() > 0) {
            // Wait for previous request to finish
            return;
        }

        messagesInFlight = messages.subList(0, Math.min(messages.size, 10));
        for( int i = 0; i < messagesInFlight.size(); i++ )
        {
            messages.remove(0);
        }

        sendViaHTTP(messagesInFlight, new Callback() {
            @Override
            public void run() {
                messagesInFlight.clear();
            }
        });
    }
}

这在我的代码中用于分析目的。我每隔10秒就会从计时器呼叫messageQueue.send(),每当发生感兴趣的事件时,我都会呼叫messageQueue.add()。此类主要用于 *** -我可以添加消息并通过HTTP发送消息,并且当HTTP请求完成时,将运行回调

问题出在计时器的第二个滴答声上。当我打到if (messagesInFlight.size() > 0) {行时,出现以下错误:

java.util.ConcurrentModificationException
        at java.util.ArrayList$SubList.size(ArrayList.java:1057)

似乎我无法在一个线程(第二个计时器的回调)中读取数组的.size(),因为它认为该数组仍在被另一个线程(第一个计时器的回调)修改。但是,我希望在调用sendViaHTTP之后,第一个计时器的线程被销毁并清理掉,因为没有其他代码可以执行。此外,HTTP请求将在500毫秒内完成,因此整整9.5秒过去了,没有任何东西触及到空的messagesInFlight数组

有没有办法说“嘿,我已经完成了对该数组的修改,人们现在可以安全地读取它了”?还是组织我的代码的更好方法?

2 个答案:

答案 0 :(得分:5)

您遇到的最明显的问题是,您在使用ArrayList.subList()时似乎并不了解它的真正作用:

  

返回此列表部分的视图...返回的列表由该列表支持。

您存储在messagesInFlight中的是一个视图,而不是一个副本。当您从messages删除第一条消息时,实际上是在messagesInFlight通话后立即删除subList()中的消息。因此,在for循环之后,将出现完全不同的消息,并且前 n 条消息将完全丢失。


为什么会出现错误,您会看到-subList()专门允许对子列表和原始列表进行非结构性更改(非结构性表示替换元素,而不添加或删除它们),并且文档中的示例还展示了如何通过修改子列表来修改原始列表。但是,不允许修改原始列表,然后再通过子列表访问它,这可能会导致ConcurrentModifcationException,与更改使用迭代器迭代的列表时发生的情况类似。

答案 1 :(得分:0)

如果您需要确保如果之前的调用未完成,则不会调用send方法,则可以将消息放入fly集合中,而只需使用标志变量即可:

private AtomicBoolean inProgress = new AtomicBoolean(false);

public void send() {

    if(inProgress.getAndSet(true)) {
        return;
    }

    // Your logic here ...  

    sendViaHTTP(messagesInFlight, new Callback() {
        @Override
        public void run() {

            inProgress.set(false);
        }
    });

}