克隆的 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
也不是,add
或 remove
也不是。
为了说明这是如何发生的,我们假设您正在使用 OpenJDK 11。 ArrayList.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
。记住我们的size
比elementData.length
多了一个,所以这个条件最终还是要满足的
当你修改数组列表时,你应该锁定它,当你克隆它或将它连接到字符串时,你也应该锁定它。
我有一个字符串 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
也不是,add
或 remove
也不是。
为了说明这是如何发生的,我们假设您正在使用 OpenJDK 11。 ArrayList.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
。记住我们的size
比elementData.length
多了一个,所以这个条件最终还是要满足的
当你修改数组列表时,你应该锁定它,当你克隆它或将它连接到字符串时,你也应该锁定它。