簡體   English   中英

序列化數組列表時 SynchronizedCollection 失敗

[英]Failed for SynchronizedCollection while serializing an array list

這是錯誤堆棧,

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)

我的 ArrayList 由 SynchronizedCollection 包裝。 並且從錯誤棧判斷ArrayList的writeObject方法是在scope的SynchronizedCollection.writeObject()方法內部調用的,后面是同步的。 似乎其他線程更改了 ArrayList,即使所有操作都由synchronized包裝。 所以,我想知道為什么其他線程可以更改 ArrayList?

該問題源於一個Android APP。 所以我寫了一個 java 程序來重現這個問題,

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));
        }
    }
}

這是示例代碼的錯誤堆棧跟蹤,

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)

這是令人驚訝的行為,並且似乎是一個 JDK 錯誤,從 OpenJDK 19.0.2 開始仍未修復。

https://bugs.openjdk.org/browse/JDK-8208779

出現此問題是因為Collections.synchronizedList(new ArrayList<>()返回的 object 是SynchronizedRandomAccessList的實例。出於向后兼容的原因,此 class 實現了一個writeReplace方法,該方法在寫入之前替換SynchronizedList的實例。然后,它是writeObject替換列表中調用以生成序列化數據的方法。不幸的是,此替換列表中使用的互斥鎖與原始列表中使用的互斥鎖不同,因此在writeObject期間持有的鎖與在add期間持有的鎖不同。這這就是為什么並發修改是可能的。

作為解決方法,您可以顯式同步列表上的writeObject調用:

synchronized (list) {
    oos.writeObject(list);
}

這樣就避免了錯誤。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM