克隆的 ArrayList 上的 joinToString 如何抛出 ConcurrentModificationException

How can joinToString on a cloned ArrayList throw ConcurrentModificationException

我有一个字符串 ArrayList,它由非 UI 线程不断更改(添加和删除行)。 在 UI 中,我想在一个简单的 TextView 中显示该 ArrayList 的当前内容。

当通过 joinToString("\n") 为 TextView 准备文本时,我立即收到一个 ConcurrentModificationException,这并不奇怪。

因此,为了避免这种情况,我克隆了 ArrayList,然后将其加入到字符串中:

val textLines = globalTextLineArray.clone() as ArrayList<String>
myTextView.text = textLines.joinToString("\n")

我不明白的是,即使现在我仍时不时收到 ConcurrentModificationException。

怎么会这样?

StackTrace 是:

java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.next(ArrayList.java:866)
    at kotlin.collections.CollectionsKt___CollectionsKt.joinTo(_Collections.kt:3341)
    at kotlin.collections.CollectionsKt___CollectionsKt.joinToString(_Collections.kt:3361)
    at kotlin.collections.CollectionsKt___CollectionsKt.joinToString$default(_Collections.kt:3360)
    at MyActivity.updateTextInView(MyActivity.kt:79)
    at MyActivity.access$updateTextInView(MyActivity.kt:24)
    at MyActivity$textViewUpdater.run(MyActivity.kt:85)
    at android.os.Handler.handleCallback(Handler.java:789)
    at android.os.Handler.dispatchMessage(Handler.java:98)
    at android.os.Looper.loop(Looper.java:164)
    at android.app.ActivityThread.main(ActivityThread.java:6944)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:327)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1374)

可能有更好的方法来实现我正在尝试做的事情(例如使用线程安全的集合)。但我还是想明白,为什么克隆尝试不起作用。

您没有锁定任何东西,因此 UI 线程上的 clone 与另一个线程上的 remove/add 同时执行。代码可以是任何顺序的 运行。这很糟糕,因为您可以在删除元素 的过程中克隆 的数组列表 - 处于中间无效状态。当您尝试迭代那个错误克隆的对象时,迭代器会感到困惑并认为您正在同时修改它。

简而言之,ArrayList不是线程安全的。它不作任何保证。您似乎明白 joinToString 不是线程安全的。好吧,clone 也不是,addremove 也不是。

为了说明这是如何发生的,我们假设您正在使用 OpenJDK 11ArrayList.remove 方法,除其他外,减少 size 字段:

// line 670
final int newSize;
if ((newSize = size - 1) > i)
    System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null; // updates size field

现在让我们看看 clone 做了什么:

// line 373
public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

让我们尝试设计一种会导致克隆无效数组列表的情况。

首先,它调用 super.clone。想象一下,这发生在 size = newSize 之前,因此复制删除之前的旧大小。然后,clone调用Arrays.copyOf复制内部数组。请注意,在这一行中,它再次访问 size。想象一下,这次访问是在size = newSize之后完成的,所以我们得到了新的更小的size。我们现在拥有的是一个包含 size > elementData.length!

的数组列表

当你迭代这个时会发生什么?迭代器 Itr 实现如下:

// line 990
public boolean hasNext() {
    return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

它将保持迭代器直到 cursor == size,并注意在 next 中它如何检查 i >= elementData.length。记住我们的sizeelementData.length多了一个,所以这个条件最终还是要满足的


当你修改数组列表时,你应该锁定它,当你克隆它或将它连接到字符串时,你也应该锁定它。