[英]java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
[英]java.lang.IndexOutOfBoundsException: Index: 0, Size: 1
我有我能想到的最奇怪的索引问题。 我有以下无辜的代码:
int lastIndex = givenOrder.size() - 1;
if (lastIndex >= 0 && givenOrder.get(lastIndex).equals(otherOrder)) {
givenOrder.remove(lastIndex);
}
对我来说似乎是一个适当的预检查。 (这里的列表被声明为List
,因此无法直接访问最后一个元素,但是对于该问题而言,这并不重要。)我得到以下堆栈跟踪:
java.lang.IndexOutOfBoundsException: Index: 0, Size: 1
at java.util.ArrayList.rangeCheck(ArrayList.java:604) ~[na:1.7.0_17]
at java.util.ArrayList.remove(ArrayList.java:445) ~[na:1.7.0_17]
at my.code.Here:48) ~[Here.class:na]
在运行时,它是一个简单的ArrayList
。 现在,索引0应该在范围之内!
许多人建议引入同步将解决此问题。 我毫不怀疑。 但是,我的这个问题(绝对没有表达)的核心是不同的:这种行为怎么可能?
让我详细说明。 这里有5个步骤:
lastIndex
(此处大小为1) ArrayList
检查边界,发现边界不足 ArrayList
构造异常消息并抛出 严格来说,粒度甚至可以更好。 现在,它可以按预期运行50,000次,没有并发问题。 (坦率地说,我什至没有找到其他可以修改该列表的地方,但是代码太大,无法排除。)
然后,它一次破裂。 对于并发问题,这是正常的。 但是,它以完全出乎意料的方式中断。 在步骤2之后到步骤4之前的某个位置,清空列表。 我希望看到一个异常,说IndexOutOfBoundsException: Index: 0, Size: 0
,这已经够糟糕了。 但是最近几个月我从未见过这样的例外!
相反,我看到IndexOutOfBoundsException: Index: 0, Size: 1
,这意味着在步骤4之后但在步骤5之前,该列表获得了一个元素。 尽管这是可能的,但似乎与上述现象不太一样。 但是,每次发生错误都会发生! 作为数学家,我说这是非常不可能的。 但是我的常识告诉我,还有另一个问题。
此外,查看ArrayList
中的代码,您会看到在那里运行了数百次的非常短的函数,并且在任何地方都没有volatile变量。 这意味着我非常希望热点编译器能够消除函数调用,从而使关键部分变得更小。 并避免了对size
变量的双重访问,从而无法观察到行为。 显然,这没有发生。
因此,我的问题是,为什么这一切可能发生,为什么它以这种奇怪的方式发生。 建议同步不是问题的答案(它可能是问题的解决方案,但这是另一回事)。
所以我检查了rangeCheck
ArrayList
实现的源代码-抛出异常的方法,这是我发现的内容:
private void rangeCheck(int paramInt) //given index
{
if (paramInt < this.size) // compare param with list size
return;
throw new IndexOutOfBoundsException(outOfBoundsMsg(paramInt)); // here we have exception
}
和相关的outOfBoundsMsg
方法
private String outOfBoundsMsg(int paramInt)
{
return "Index: " + paramInt + ", Size: " + this.size; /// OOOoo we are reading size again!
}
如您所见,列表的大小( this.size
)被访问了2次。 第一次读取该消息以检查条件并且该条件未满足,因此将为异常生成消息。 在为异常创建消息时,仅paramInt
在paramInt
调用之间是持久的,但是列表的大小是第二次读取。 这是我们的罪魁祸首。
实际上,您应该得到Message: Index:0 Size:0
,但是用于检查的大小值不是本地存储的(微优化)。 因此,这两次读取之间this.size
列表已更改。
这就是为什么消息令人误解的原因。
结论:这种情况在高度同时存在的环境中是可能的,并且可能很难重现。 要解决该问题,请使用ArrayList
synchronized
版本(建议使用@JordiCastillia)。 该解决方案可能会影响性能,因为每个操作(添加/删除和可能获取)都将被synchronized
。 其他解决方案是将您的代码放入synchronized
块中,但这只会同步您在这段代码中的调用,并且由于系统的不同部分仍然可以异步访问整个对象,因此将来仍然可能出现问题。
这很可能是并发问题。
在尝试访问索引之前/之后,大小会有所修改。
使用Collections.synchronizedList()
。
经过一个简单的main
测试,它的工作原理是:
List<String> givenOrder = new ArrayList<>();
String otherOrder = "null";
givenOrder.add(otherOrder);
int lastIndex = givenOrder.size() - 1;
if (lastIndex >= 0 && givenOrder.get(lastIndex).equals(otherOrder)) {
System.out.println("remove");
givenOrder.remove(lastIndex);
}
您是否正在执行thread-safe
过程? 您的List
已被其他线程或进程修改。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.