Reputation: 2530
I have an ArrayList of Strings, which is constantly changed (adding and removing lines) by a non-UI thread. In the UI I want to show the current content of that ArrayList in a simple TextView.
When preparing the text for the TextView by joinToString("\n"), I was immediately receiving a ConcurrentModificationException, which was not very surprising.
So, to avoid that, I cloned the ArrayList, before joining it to a String:
val textLines = globalTextLineArray.clone() as ArrayList<String>
myTextView.text = textLines.joinToString("\n")
What I am not understanding is, that even now I am receiving from time to time a ConcurrentModificationException.
How can that happen?
The StackTrace is:
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$1.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)
There may be better ways to achieve what I am trying to do (e.g. using a thread-safe collection). But still I would like to understand, why the cloning attempt does not work.
Upvotes: 2
Views: 515
Reputation: 273978
You are not locking anything, so clone
on the UI thread is executed at the same time as remove
/add
on the other thread. The code could be run in any order. This is bad, because you could cloning an array list that is in the middle of removing an element - in an intermediate, invalid state. When you try to iterate on that incorrectly cloned object, the iterator is confused and thinks you are modifying it concurrently.
In short, ArrayList
is not thread safe. It does not make any guarantees. You seem to understand that joinToString
isn't thread safe. Well, clone
isn't either, and neither is add
or remove
.
To illustrate how this can happen, let's assume you're using OpenJDK 11. The ArrayList.remove
method, among other things, decrease the size
field:
// 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
Now let's look at what clone
does:
// 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);
}
}
Let's try to contrive a situation that will cause an invalid array list to be cloned.
First, it calls super.clone
. Imagine that this happens before size = newSize
, so the old size before the removal gets cloned. Then, clone
calls Arrays.copyOf
to copy the internal array. Note that in this line, it accesses size
again. Imagine that this time, the access is done after size = newSize
, so we get the new smaller size
. What we have now is an array list with size > elementData.length
!
What happens when you iterate over this? The iterator Itr
implemented like this:
// 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];
}
It will keep iterator until cursor == size
,and note how in next
, it checks for i >= elementData.length
. Remember that our size
is one more than elementData.length
, so this condition will eventually be satisfied.
When you modify the array list, you should lock it, and when you clone it or join it to string, you should also lock it.
Upvotes: 2