我最近遇到了一个意外错误(在属于我的代码中),该错误导致ArrayIndexOutOfBoundsException
上出现ArrayList#contains
。此处的相关代码如下。
private static final List<String> list = new ArrayList<>();
static void register() {
update();
new Timer().schedule(new TimerTask() {
@Override
public void run() {
update();
}
}, 0, 21600000);
}
private static void update() {
list.clear();
new Thread(() -> {
List<String> other; //should always be the same length.
list.addAll(other);
}).start();
}
public static boolean contains(String string) { //called long after register
return list.contains(string); //throws ArrayIndexOutOfBounds
}
我很清楚ArrayList
不是线程安全的,可以用Collections#synchronizedList
之类的东西来修复它。我的问题是了解此特定代码如何引发ArrayIndexOutOfBoundsException
。
该异常的堆栈跟踪在ArrayList#indexOf
中标识以下代码。
for (int i = 0; i < size; i++)
if (o.equals(elementData[i])) //here
return i;
在我看来,只有在size
大于elementData.length
时才会发生这种情况。据我了解,ArrayList#clear
实际上并没有减少支持数组的长度。对addAll
的调用仅应增加其容量,并且size
总是在数组扩展后更新。我不知道在size
大于数组容量的状态下,这种情况怎么可能。
我注意到的一个具体细节是Timer
的延迟为0,这意味着update()
被连续快速调用两次。我最好的猜测是addAll
调用在某种程度上是重叠的,这使列表成为某种混合的无效状态。
如果有人可以解释这里发生的事情,那就太好了!
答案 0 :(得分:2)
如果没有适当的同步,则不能保证关于一个线程的更改在另一个线程中可见。
无保证表示,如果线程A更改elementData
和size
,线程B看到:
size
,但旧的elementData
elementData
,但旧的size
elementData
的内容未更新为什么会发生这种情况(简短介绍请参见Wikipedia:Java Memory Model)
在现代计算机中,主内存相对较慢,因此在主内存和执行当前线程的CPU内核之间存在多个高速缓存。
线程A可能会在CPU寄存器或几个高速缓存之一中保持对size
和elementData
的更新,并在以后某个时间(甚至对于这些字段在不同的时间)更新主内存-只要线程A的正确性不受影响。由于没有同步发生,因此多线程的正确性无关紧要-线程A的工作就像是访问这些字段的唯一线程。
类似地,线程B可能将size
和elementData
保留在CPU寄存器或高速缓存中,或者它可以根据需要从主内存中更新一个或两个(可能是{{1} }已保存,已清除,因此需要从主存储器中重新加载size
。由于没有同步发生,线程B假定没有其他线程更改这些字段,因此缓存的值始终与主内存中的值相同。