序列化SynchronizedCollection包装的ArrayList时抛出ConcurrentModificationException
你遇到的ConcurrentModificationException,核心问题出在**Collections.synchronizedCollection的默认序列化逻辑没有在整个序列化流程中持有锁**。
看一下Collections$SynchronizedCollection的writeObject源码实现:
private void writeObject(ObjectOutputStream s) throws IOException { synchronized (mutex) { s.defaultWriteObject(); } }
这个方法只在执行s.defaultWriteObject()时加锁——这一步仅仅是把SynchronizedCollection自身的字段(比如底层的ArrayList实例c)写入流。等这行代码执行完毕,锁就被释放了。
而真正序列化底层ArrayList的操作,是在锁释放之后才触发的:ObjectOutputStream接下来会去序列化刚才写出的c字段,调用ArrayList自己的writeObject方法。这时候其他线程完全可以调用list.add()(因为add方法的同步锁是同一个mutex,但锁已经被释放),导致ArrayList在序列化过程中被修改,触发ConcurrentModificationException。
要解决这个问题,需要确保整个序列化过程都持有SynchronizedCollection的锁,有两种可行的实现方式:
1. 序列化前手动加锁
在调用oos.writeObject(list)前后手动获取并释放锁,把整个序列化过程包裹在锁的保护范围内:
// 修改WriteListTask的run方法 @Override public void run() { File file = new File("temp"); OutputStream os = null; ObjectOutputStream oos = null; try { os = new FileOutputStream(file); oos = new ObjectOutputStream(os); // 手动获取锁,list是SynchronizedList,锁对象就是自身 synchronized (list) { oos.writeObject(list); } oos.flush(); os.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (oos != null) oos.close(); if (os != null) os.close(); } catch (IOException e) { e.printStackTrace(); } } System.out.println(String.format("write [%d] list job done", ++writeTaskNo)); }
这样整个序列化流程(包括底层ArrayList的writeObject执行)都会在锁的保护下,其他线程的修改操作会被阻塞,直到序列化完成。
2. 自定义同步集合包装类
如果不想每次序列化都手动加锁,可以自己实现一个集合包装类,让它的序列化逻辑全程持有锁:
import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.List; import java.util.Objects; public class SynchronizedSerialList<E> implements List<E>, Serializable { private final List<E> delegate; private final Object mutex; public SynchronizedSerialList(List<E> delegate) { this.delegate = Objects.requireNonNull(delegate); this.mutex = this; } // 实现所有List接口方法,都用synchronized(mutex)包裹,参考Collections.synchronizedList的实现 @Override public boolean add(E e) { synchronized (mutex) { return delegate.add(e); } } // 省略其他List方法的实现,均按上述方式同步 // 自定义writeObject,确保序列化全程持有锁 private void writeObject(ObjectOutputStream s) throws IOException { synchronized (mutex) { s.writeObject(delegate); } } private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { synchronized (mutex) { delegate = (List<E>) s.readObject(); } } }
然后把原来的Collections.synchronizedList替换成这个自定义类:
private static final List<String> list = new SynchronizedSerialList<>(new ArrayList<>());
修改后运行你的测试代码,就不会再出现ConcurrentModificationException了——序列化全程被锁保护,其他线程的修改操作会被阻塞直到序列化完成。
内容的提问来源于stack exchange,提问作者Shouheng Wang

