CopyOnWriteArrayList就是最常说的写时复制容器,虽然在项目中使用场景不是很多但是某些特定环境下会得到不错的性能提升。简单来说它的原理就跟名字一样写时复制,当我们往一个容器添加元素的时候,并不会不直操作容器进行修改,而是先将当前容器进行Copy,复制出一个容器的副本,然后在赋值出的这个容器上进行修改,修改完成之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyO nWrite容器进行并发的读取时候是不会产生加锁的,因为当前容器不会添加任何元素。在写相对比较少,且对时效性要求不高的并发环境下可以使用juc下的这种容器来减小锁的粒度。
读取:
我们可以看到在获取值时会直接返回数组对象并不会进行加锁,所以当我们在进行获取时如果有线程正在进行修改操作那么我们读到的就是旧数据,这其实就是写时复制策略产生的弱一致性问题。
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
public E get(int index) {
return get(getArray(), index);
}
修改操作
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
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();
}
}
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1]; // —创建一个新的数组
System.arraycopy(elements, 0, newElements, 0, index); // 复制index之前的数据到新数组
System.arraycopy(elements, index + 1, newElements, index,
numMoved);/ // 复制index之后的数组到index位置
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
我们可以看到在进行增加、删除等操作时是需要获取锁的,并且需要进行复制如果修改操作多了这样势必会十分消耗内存。在修改操作较多的情况下并不建议使用。在spring中正是使用CopyOnWriteArrayList容器来存放后置处理器的。
迭代器
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot; // array的快照
/** Index of element to be returned by subsequent call to next. */
// 对next的后续调用将返回的元素的索引
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
public void remove() {
throw new UnsupportedOperationException();
}
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
@Override
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
Object[] elements = snapshot;
final int size = elements.length;
for (int i = cursor; i < size; i++) {
@SuppressWarnings("unchecked") E e = (E) elements[i];
action.accept(e);
}
cursor = size;
}
}
在调用iterator其实给我返回的是一个COWIterator静态内部类,COWIterator对象的snapshot变量保存了当前list的内容,cursor是遍历list数组元素的下标此构造方法有两个参数,第一个参数就是 array 数组,第二个参数是下标,就是 0。随后构造方法中会把 array 数组赋值给snapshot变量。这里数组是引用类型传递的是指针,如果有其他地方修改了数组,这里应该马上就可以反应出来。那为什么又会是 snapshot这样的命名呢?没错,如果其他线程没有对 CopyOnWriteArrayList 进行增删改的操作,那么 snapshot 就是本身的array,但是如果其他线程对 CopyOnWriteArrayList 进行了增删改的操作,旧的数组会被新的数组给替换掉,但是 snapshot 还是原来旧的数组的引用。也就是说 当我们使用迭代器便利 CopyOnWriteArrayList 的时候,不能保证拿到的数据是最新的,这也是一致性问题。所以在对数据时效性有很高要求且修改频繁的场景下CopyOnWriteArrayList 并不适用这也是它的两个明显缺点。