我有一个我能想象的最好奇的索引问题。我有以下无辜的代码:
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
变量的双重访问,使观察到的行为变得不可能。显然,这种情况并没有发生。
所以,我的问题是为什么会发生这种情况以及为什么它会以这种奇怪的方式发生。建议同步不是问题的答案(它可能是问题的解决方案,但这是另一回事)。
答案 0 :(得分:2)
所以我检查了ArrayList
rangeCheck
实现的源代码 - 抛出异常的方法,这就是我发现的:
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
在调用之间是持久的,但是第二次读取列表的大小。在这里,我们有罪魁祸首。
实际上,您应该获得Message:Index:0 Size:0
,但是用于检查的大小值不是本地存储的(微优化)。所以这两个this.size
列表的读取之间已经改变了。
这就是消息具有误导性的原因。
Conclussion:
这种情况在高度并发的环境中是可能的,并且可能非常难以再现。要解决该问题,请使用synchronized
ArrayList
版本(如@JordiCastillia建议的那样)。此解决方案可能会对性能产生影响,因为每个操作(添加/删除和可能获取)都将为synchronized
。其他解决方案是将您的代码放入synchronized
块,但这只会在这段代码中同步您的调用,并且将来仍会出现问题,因为系统的不同部分仍然可以访问整个对象异步。
答案 1 :(得分:0)
这很可能是并发问题。
在您尝试访问索引之前/之后,大小会以某种方式修改。
答案 2 :(得分:0)
使用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
被其他一些线程或进程修改。