【问题标题】:ArrayIndexOutOfBounds on ArrayList#contains (multithreaded)ArrayList#contains 上的 ArrayIndexOutOfBounds(多线程)
【发布时间】:2018-06-27 04:20:29
【问题描述】:

我最近遇到了一个意外错误(在不是我的代码中)导致ArrayList#contains 上的ArrayIndexOutOfBoundsException。这里的相关代码如下。

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 调用在某种程度上重叠,这使得列表中出现某种混合无效状态。

如果有人能解释这里发生了什么,那就太好了!

【问题讨论】:

  • 请张贴围绕for循环的整个方法。我只能猜测oelementData 代表什么值。
  • 根据 Java 文档:如果多个线程同时访问 ArrayList 实例,并且至少有一个线程在结构上修改了列表,则它必须在外部同步。 (结构修改是添加或删除一个或多个元素,或显式调整后备数组大小的任何操作;仅设置元素的值不是结构修改。)
  • @DiabolicWords 直接来自ArrayList#indexOf,如上所述。我不明白这会如何真正影响这一点。 hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/… @smitty1 是的,我知道。我的问题主要与如何发生有关,尤其是了解多线程如何在基础层面上工作。

标签: java multithreading arraylist


【解决方案1】:

如果没有适当的同步,则无法保证来自一个线程的哪些更改在另一个线程中可见。

无保证意味着如果线程 A 更改 elementDatasize,线程 B 看到:

  • 没有任何变化
  • 更新的size,但旧的elementData
  • 更新的elementData,但旧的size
  • 两个值都更新了,但elementData的内容没有更新

至于为什么会发生这种情况(请参阅Wikipedia:Java Memory Model 的简短介绍):

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

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

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

【讨论】:

  • 我相信这部分回答了这个问题。您是否有资源可以验证这一点并提供更多详细信息来解释多线程如何不匹配来自不同线程的变量?
猜你喜欢
  • 1970-01-01
  • 2016-11-10
  • 2013-07-06
  • 1970-01-01
  • 2016-05-31
  • 1970-01-01
  • 1970-01-01
  • 2019-06-07
  • 1970-01-01
相关资源
最近更新 更多