java.lang.IndexOutOfBoundsException:索引:0,大小:1

java.lang.IndexOutOfBoundsException:索引:0,大小:1,java,Java,我有一个我能想象到的最奇怪的索引问题。我有以下看似无辜的代码: int lastIndex = givenOrder.size() - 1; if (lastIndex >= 0 && givenOrder.get(lastIndex).equals(otherOrder)) { givenOrder.remove(lastIndex); } 在我看来,这是一次适当的预检。(此处的列表声明为list,因此无法直接访问最后一个元素,但这对问题来说并不重要。)我得到以

我有一个我能想象到的最奇怪的索引问题。我有以下看似无辜的代码:

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
    构造异常消息并抛出
  • 严格地说,粒度可能更细。现在,它按预期工作了50000次,没有并发问题。(坦率地说,我甚至没有找到任何其他地方可以修改该列表,但代码太大,无法排除这种可能性。)

    然后,有一次它坏了。这对于并发问题来说是正常的。然而,它以一种完全出乎意料的方式中断了。在步骤2之后和步骤4之前的某个地方,列表被清空。我希望出现一个异常,即
    IndexOutOfBoundsException:Index:0,Size:0
    ,这已经够糟糕了。但在过去的几个月里,我从未见过这样的例外

    相反,我看到的是
    IndexOutOfBoundsException:Index:0,Size:1
    ,这意味着在步骤4之后但在步骤5之前,列表将获得一个元素。虽然这是可能的,但似乎不太可能出现上述现象。然而,每次错误发生时都会发生!作为一名数学家,我认为这是不可能的。但我的常识告诉我还有另一个问题

    此外,查看
    ArrayList
    中的代码,您可以看到那里运行了数百次的非常短的函数,并且没有任何可变变量。这意味着我非常希望hotspot编译器能够省略函数调用,使关键部分更小;而且,该方法省略了对
    大小
    变量的双重访问,使得观察到的行为不可能。显然,这并没有发生


    所以,我的问题是,为什么会发生这种情况,为什么会以这种奇怪的方式发生。建议同步并不是问题的答案(它可能是问题的解决方案,但这是另一回事)。

    这很可能是并发问题

    在尝试访问索引之前/之后,大小会以某种方式被修改。

    请使用

    在一个简单的
    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);
    }
    
    List givenOrder=new ArrayList();
    字符串otherOrder=“null”;
    添加(其他订单);
    int lastIndex=givenOrder.size()-1;
    if(lastIndex>=0&&givenOrder.get(lastIndex.equals)(其他顺序)){
    系统输出打印项次(“删除”);
    删除(lastIndex);
    }
    

    您是否处于线程安全进程中?您的
    列表
    被其他线程或进程修改

    因此,我检查了引发异常的
    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
    }
    
    正如您可能看到的,列表的大小(
    this.size
    )被访问了2次。第一次读取它是为了检查条件,而条件没有完全填充,因此消息是为异常生成的。在为异常创建消息时,调用之间只有
    paramInt
    是持久的,但列表的大小是第二次读取的。这就是我们的罪魁祸首

    实际上,您应该得到消息:
    索引:0大小:0
    ,但用于检查的大小值不是本地存储的(微优化)。因此,在这两次读取之间,
    此.size
    列表已更改

    这就是信息误导的原因

    结论:
    这种情况在高度并发的环境中是可能的,并且很难重现。要解决此问题,请使用
    synchronized
    版本的
    ArrayList
    (如@JordiCastillia)。此解决方案可能会影响性能,因为每个操作(添加/删除和可能获取)都将
    同步。另一种解决方案是将您的代码放入
    synchronized
    块,但这只会同步这段代码中的调用,而且问题在将来仍然会发生,因为系统的不同部分仍然可以异步访问整个对象。

    共享
    列表
    ,请先添加一个元素,然后再检查ArrayList是否为空。另一个进程或线程正在修改ArrayList。我想你正在循环中使用它,而这个循环比你想象的要多。向我们显示您的循环。您必须引入此列表的
    已同步版本
    或至少同步您发布的代码。这是一个并发问题,它揭示了
    ArrayList
    internalst中的某种错误。如果@JordiCastilla回答有意义,则另一个线程可以在创建lastIndex后修改列表assigned@Antoniossss大小和错误不在同一行中,因此,如果另一个线程修改了
    列表
    ,则当列表中的对象正在运行时,此信息将不正确removed@GuillermoMerino我只是不明白,
    lastIndex
    与我无关,只有那个例外。@GuillermoMerino在我看来,这是最可能的并发问题,但这里的例外不仅仅是误读,因为给定的索引是有界的。不幸的是,您的简单测试太简单了,因为它几乎总是有效的。很可能,而且不知何故,它没有回答这个问题。这很可能是一个并发问题
    private String outOfBoundsMsg(int paramInt)
    {
        return "Index: " + paramInt + ", Size: " + this.size; /// OOOoo we are reading size again!
    }