Here is the error stack,
java.util.ConcurrentModificationException
at java.util.ArrayList.writeObject(ArrayList.java:766)
at java.lang.reflect.Method.invoke(Native Method)
at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:977)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1545)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1481)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1227)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1597)
at java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.java:456)
at java.util.Collections$SynchronizedCollection.writeObject(Collections.java:2125) // 1
at java.lang.reflect.Method.invoke(Native Method)
at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:977)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1545)
My ArrayList is wrapped by SynchronizedCollection. And judged from the error stack that the writeObject
method of ArrayList is called inside the scope of SynchronizedCollection.writeObject()
method and the later is synchronized. It seems that other threads changed the ArrayList, even though all actions are wrapped by synchronized
. So, I wonder why other threads could change the ArrayList?
This issue arise from an Android APP. So I write a java program to reproduce this issue,
public class MultiThreadSerialTest2 {
private static final int TOTAL_TEST_LOOP = 100;
private static final int TOTAL_THREAD_COUNT = 20;
private static volatile int writeTaskNo = 0;
private static final List<String> list = Collections.synchronizedList(new ArrayList<>());
private static final Executor executor = Executors.newFixedThreadPool(TOTAL_THREAD_COUNT);
public static void main(String...args) throws IOException {
for (int i = 0; i < TOTAL_TEST_LOOP; i++) {
executor.execute(new WriteListTask());
for (int j=0; j<TOTAL_THREAD_COUNT-1; j++) {
executor.execute(new ChangeListTask());
}
}
}
private static final class ChangeListTask implements Runnable {
@Override
public void run() {
list.add("hello");
System.out.println("change list job done");
}
}
private static final class WriteListTask implements Runnable {
@Override
public void run() {
File file = new File("temp");
OutputStream os = null;
ObjectOutputStream oos = null;
try {
os = new FileOutputStream(file);
oos = new ObjectOutputStream(os);
oos.writeObject(list);
oos.flush();
os.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
oos.close();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println(String.format("write [%d] list job done", ++writeTaskNo));
}
}
}
Here is the error stacktrace of the sample code,
Exception in thread "pool-1-thread-88" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList.writeObject(ArrayList.java:901)
at java.base/jdk.internal.reflect.GeneratedMethodAccessor4.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at java.base/java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1145)
at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1497)
at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1433)
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1179)
at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1553)
at java.base/java.io.ObjectOutputStream.defaultWriteObject(ObjectOutputStream.java:442)
at java.base/java.util.Collections$SynchronizedCollection.writeObject(Collections.java:2086)
at java.base/jdk.internal.reflect.GeneratedMethodAccessor2.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at java.base/java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1145)
at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1497)
at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1433)
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1179)
at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:349)
at me.shouheng.thread.MultiThreadSerialTest2$WriteListTask.run(MultiThreadSerialTest2.java:49)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
This is surprising behaviour, and appears to be a JDK bug, still not fixed as of OpenJDK 19.0.2.
https://bugs.openjdk.org/browse/JDK-8208779
The issue arises because the object returned by Collections.synchronizedList(new ArrayList<>()
is an instance of SynchronizedRandomAccessList
. This class implements a writeReplace
method that substitutes an instance of SynchronizedList
before writing, for backward compatibility reasons. Then, it is the writeObject
method in the substituted list that is called to produce the serialised data. Unfortunately, the mutex used in this substituted list is different than the one used in the original list, so the lock held during writeObject
is different than the one held during add
. This is why concurrent modification is possible.
As a workaround, you can explicitly synchronize the writeObject
call on the list:
synchronized (list) {
oos.writeObject(list);
}
This avoids the error.