繁体   English   中英

java.lang.IndexOutOfBoundsException:索引:0,大小: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个步骤:

  1. 我检查大小并计算lastIndex (此处大小为1)
  2. 我什至访问了最后一个元素
  3. 我要求移除
  4. ArrayList检查边界,发现边界不足
  5. 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次。 第一次读取该消息以检查条件并且该条件未满足,因此将为异常生成消息。 在为异常创建消息时,仅paramIntparamInt调用之间是持久的,但是列表的大小是第二次读取。 这是我们的罪魁祸首。

实际上,您应该得到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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM