最近在读 Tomcat 源码时发现,存放容器事件监听器 ContainerListener 的容器使用的是 CopyOnWriteArrayList,于是忍不住想去了解它的实现原理,这里同大家一起分享下。
首先我们从集合框架的体系图开始,先对 CopyOnWriteArrayList 在集合框架中的层次和位置有一个了解。
1. 关键属性及构造函数
/** The array, accessed only via getArray/setArray. */
// 存放元素的数组
private transient volatile Object[] array;
// 无参的构造,初始化一个长度为零的数组
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
2. add()
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 修改操作使用了锁,因此事线程安全的
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 每次新创建一个长度为当前长度加一的新数组,并把当前数组的元素复制进去,
// 这也是 CopyOnWriteArrayList 名称的由来。
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 重置数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
2. remove()
public boolean remove(Object o) {
Object[] snapshot = getArray();
// 遍历查找要删除元素所在的索引位置
int index = indexOf(o, snapshot, 0, snapshot.length);
// 不存在返回 false,存在则进行线程安全的删除
return (index < 0) ? false : remove(o, snapshot, index);
}
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
// 线程安全的操作
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
// 传入的数组快照和当前数组不等,说明数组已经被改变
if (snapshot != current) findIndex: {
int prefix = Math.min(index, len);
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
// index >= len 说明最新的数组中已经不包含要删除的元素,
// 因此本次删除操作失败。
if (index >= len)
return false;
// 最新的数组中的元素与要删除的元素相等,删除成功,跳出 if,执行后续处理
if (current[index] == o)
break findIndex;
// 以上条件均不成立,说明数组刚刚进行过删除操作,导致要删除的元素位置发生变化,
// 进而导致上面 for 循环查找失败,因此这里重新查找
index = indexOf(o, current, index, len);
// 查不到,说明元素已经被删除,本次删除操作失败,查找到则执行 if 之后的操作
if (index < 0)
return false;
}
// 将要删除的元素从数组中移除:复制时跳过要删除的元素即可
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
3. 总结
- CopyOnWriteArrayList 每次修改操作时,都会复制一个新的数组(这样会带来时间、空间的开销)。
- CopyOnWriteArrayList 对数底层组的修改操作(包括插入和删除)是线程安全的,插入和删除操作使用的是同一把锁。
- 底层的数组使用 volatile 修饰,通过内存屏障保证每次查询到的都是最新的数据。
4. 思考
集合框架中有诸多 List 的实现,不同的实现适用于不同的应用场景,那么 CopyOnWriteArrayList 适合怎样的使用场景呢,我们先看一下源码中给出的 CopyOnWriteArrayList 类的说明:
/**
* A thread-safe variant of {@link java.util.ArrayList} in which all mutative
* operations ({@code add}, {@code set}, and so on) are implemented by
* making a fresh copy of the underlying array.
*
* <p>This is ordinarily too costly, but may be <em>more</em> efficient
* than alternatives when traversal operations vastly outnumber
* mutations, and is useful when you cannot or don't want to
* synchronize traversals, yet need to preclude interference among
* concurrent threads. The "snapshot" style iterator method uses a
* reference to the state of the array at the point that the iterator
* was created. This array never changes during the lifetime of the
* iterator, so interference is impossible and the iterator is
* guaranteed not to throw {@code ConcurrentModificationException}.
* The iterator will not reflect additions, removals, or changes to
* the list since the iterator was created. Element-changing
* operations on iterators themselves ({@code remove}, {@code set}, and
* {@code add}) are not supported. These methods throw
* {@code UnsupportedOperationException}.
*
* <p>All elements are permitted, including {@code null}.
*
* <p>Memory consistency effects: As with other concurrent
* collections, actions in a thread prior to placing an object into a
* {@code CopyOnWriteArrayList}
* <a href="package-summary.html#MemoryVisibility"><i>happen-before</i></a>
* actions subsequent to the access or removal of that element from
* the {@code CopyOnWriteArrayList} in another thread.
*
* <p>This class is a member of the
* <a href="{@docRoot}/../technotes/guides/collections/index.html">
* Java Collections Framework</a>.
*
* @since 1.5
* @author Doug Lea
* @param <E> the type of elements held in this collection
*/
上面的官方注释中给出了答案:
每次修改列表时都会复制出一个新的列表,这种方式的成本很高。
但是当我们对列表的遍历的操作远远多于修改操作,并且遍历的操作需要同步时,使用快照风格的迭代器会更高效。
快照风格的迭代器:创建迭代器时传入当时的数组快照,迭代的操作基于快照进行,快照在迭代器的生命周期中是不可变的,因此不会出现并发带来的数据问题,同时也不会抛出 ConcurrentModificationException 异常。
ConcurrentModificationException 异常在 ArrayList 篇有做详细介绍。
我们进一步看下 CopyOnWriteArrayList 中迭代器的实现细节:
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
public ListIterator<E> listIterator() {
return new COWIterator<E>(getArray(), 0);
}
// COWIterator 虽然实现了 ListIterator 接口,但是并不支持增删改的操作。
// 与之不同的,ArrayList 中的 ListItr 也实现了 ListIterator 接口,
// 同时也实现了增删改查的操作
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
// 快照的地址值不可变,同时不对外提供对快照的数组元素的修改操作,
// 这样保证了快照在整个迭代器生命周期中的不可变特性
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;
}
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;
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code remove}
* is not supported by this iterator.
*/
// 不对外提供对快照的数组元素的修改操作
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code set}
* is not supported by this iterator.
*/
// 不对外提供对快照的数组元素的修改操作
public void set(E e) {
throw new UnsupportedOperationException();
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code add}
* is not supported by this iterator.
*/
// 不对外提供对快照的数组元素的修改操作
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;
}
}