正如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 问题更新了帖子的标题。
我的代码大致如下:
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
数组
有没有办法说“嘿,我已经完成了对该数组的修改,人们现在可以安全地读取它了”?还是组织我的代码的更好方法?
答案 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);
}
});
}