ArrayList#上的ArrayIndexOutOfBounds包含(多线程)

时间:2018-06-27 04:20:29

标签: java multithreading arraylist

我最近遇到了一个意外错误(在属于我的代码中),该错误导致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调用在某种程度上是重叠的,这使列表成为某种混合的无效状态。

如果有人可以解释这里发生的事情,那就太好了!

1 个答案:

答案 0 :(得分:2)

如果没有适当的同步,则不能保证关于一个线程的更改在另一个线程中可见。

无保证表示,如果线程A更改elementDatasize,线程B看到:

  • 所有更改
  • 更新后的size,但旧的elementData
  • 更新后的elementData,但旧的size
  • 两个值均已更新,但elementData的内容未更新

为什么会发生这种情况(简短介绍请参见Wikipedia:Java Memory Model

在现代计算机中,主内存相对较慢,因此在主内存和执行当前线程的CPU内核之间存在多个高速缓存。

线程A可能会在CPU寄存器或几个高速缓存之一中保持对sizeelementData的更新,并在以后某个时间(甚至对于这些字段在不同的时间)更新主内存-只要线程A的正确性不受影响。由于没有同步发生,因此多线程的正确性无关紧要-线程A的工作就像是访问这些字段的唯一线程。

类似地,线程B可能将sizeelementData保留在CPU寄存器或高速缓存中,或者它可以根据需要从主内存中更新一个或两个(可能是{{1} }已保存,已清除,因此需要从主存储器中重新加载size。由于没有同步发生,线程B假定没有其他线程更改这些字段,因此缓存的值始终与主内存中的值相同。