Java CopyOnWriteArrayList Copy on Write(写时复制)分析
Copy on Write这种机制用在集合上,当读取元素,先复制原有集合内容,在集合的某个副本上进行遍历操作。当发生写操作时,首先复制原有集合内容生成一个副本,随后在这个副本上进行写操作。
CopyOnWriteArrayList<Integer> ids = new CopyOnWriteArrayList<>();
ids.add(5);
for (Integer i : ids) {
System.out.println(i);
ids.add(6);
}
for (Integer i : ids) {
System.out.println(i);
}
以上代码以CopyOnWriteArrayList为例。我们知道使用foreach语法糖进行集合遍历时,实际上是通过迭代器对象来进行遍历。
CopyOnWriteArrayList内部维护了array对象,并COWIterator进行元素迭代。访问通array对象内的元素。
private transient volatile Object[] array;
当循环开始首先创建COWIterator对象,这时COWIterator内部维护的snapshot对象指向CopyOnWriteArrayList的array对象。而在循环执行过程中snapshot将不会发生变化。因此即使在循环中对CopyOnWriteArrayList进行修改,也不会影响snapshot结果。所以不会造成ConcurrentModificationException的问题。
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
当对list执行修改操作,这是会新创建一段存储空间,并拷贝原有数据。随后在这个新的副本空间上进行修改。当修改完毕执行setArray,修改
CopyOnWriteArrayList中array成员,array指向新的存储空间。而迭代器在初始化时保存的是旧的存储空间引用。因此写操作不会对当前循环发生影响。当再次循环遍历时可以看到新增加的元素输出。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
此外我们看到add操作是加了锁。当多线程操作同时发生修改操作,若不加锁会导致数据发生覆盖,添加数据丢失。
总结
Copy on write提供了一种机制,当遍历一个集合时,首先纪录集合当时的快照引用。那么遍历就是对集合某个时刻快照进行遍历。当发生写操作会生成新的副本,修改操作发生在新的副本上,而不对原有存储空间修改。这种思路下,读写处理了不同的数据存储区域, 避免了并发修改异常的问题。但这种方式也会造成数据读取时短暂不一致状态。
参考
jdk1.8 java.util.concurrent.CopyOnWriteArrayList