Java 集合框架源码解读系列--CopyOnWriteArrayList 篇

最近在读 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. 总结

  1. CopyOnWriteArrayList 每次修改操作时,都会复制一个新的数组(这样会带来时间、空间的开销)。
  2. CopyOnWriteArrayList 对数底层组的修改操作(包括插入和删除)是线程安全的,插入和删除操作使用的是同一把锁。
  3. 底层的数组使用 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;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值