CopyOnWriteArrayList源码分析

Scroll Down

前言

ArrayList 是一个非线程安全集合,需要使用方自行处理线程安全问题,或者使用 Collections.synchronizedList 包装。从 JDK 1.5 开始 JUC 中提供了使用写时复制机制实现的并发容器 CopyOnWriteArrayList。

概述

CopyOnWriteArrayList 遵从 CopyOnWrite,顾名思义就是写时复制。简单来说就是当我们操作容器时并不直接操作当前容器,而是先根据当前容器复制出一个新的容器,然后在这个新的容器上操作,完成操作后再将原容器引用指向新容器。

属性

public class CopyOnWriteArrayList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    /**
     * 保护所有更改操作的锁
     */
    final transient ReentrantLock lock = new ReentrantLock();

    /**
     * 内部数组。
     * 所有的读操作都是基于当前 array 进行的;所有的写操作都会复制一个新的 array,然后在新复制的数组上执行操作。
     */
    private transient volatile Object[] array;
}

CopyOnWriteArrayList 核心属性就是以上两个。lock 用于保护所有更改操作,控制并发写操作。array 作为内部数组用于保存数据,读操作都是操作当前 array,写操作都是根据当前 array 复制一个新的 array,然后在这个新的数组中进行操作。

新增

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 1 获得独占锁
    lock.lock();
    try {

        // 2 获得 array 数组
        Object[] elements = getArray();

        // 3 获得 array 数组长度
        int len = elements.length;

        // 4 根据当前 array 复制一个新的数组,长度 +1
        Object[] newElements = Arrays.copyOf(elements, len + 1);

        // 5 将元素加入到新数组
        newElements[len] = e;
        
        // 6 替换旧数组
        setArray(newElements);
        return true;
    } finally {
        // 7 释放独占锁
        lock.unlock();
    }
}

下面对上述代码流程进行说明:

使用 ReentrantLock 独占锁保证同时只有一个线程对集合进行写操作。
数据存储在 CopyOnWriteArrayList 的内部 array 数组中。
元素并不是直接存储到 array 数组中,而是复制出一个新的数组,并且复制出的数组的长度是旧数组长度+1,然后将元素加入到新数组中,最后再把旧的数组替换成新的数组。

查询

/**
  * {@inheritDoc}
  *
  * @throws IndexOutOfBoundsException {@inheritDoc}
  */
 public E get(int index) {
     return get(getArray(), index);
 }

 @SuppressWarnings("unchecked")
 private E get(Object[] a, int index) {
     return (E) a[index];
}

由于整个 get 方法是没有加锁的,而 CopyOnWriteArrayList 的写操作是通过复制出新的数组来完成操作的,这可能会导致读写的短暂不一致。

修改

public E set(int index, E element) {
       final ReentrantLock lock = this.lock;
       // 1 获得独占锁
       lock.lock();
       try {

           // 2 获得当前 数组 array
           Object[] elements = getArray();

           // 3 根据下标,获得旧的元素
           E oldValue = get(elements, index);

           // 4 如果旧的元素不等于新的元素,等同于 add 方法,不过这里没有增加数组容量大小
           if (oldValue != element) {
               int len = elements.length;
               Object[] newElements = Arrays.copyOf(elements, len);
               newElements[index] = element;
               setArray(newElements);

               // 5 为了保证 volatile 语义,即使元素没有改变也要替换成新的数组
           } else {
               // Not quite a no-op; ensures volatile write semantics
               setArray(elements);
           }
           return oldValue;
       } finally {

           // 6 释放独占锁
           lock.unlock();
       }
   }

和 add 方法类似,不同的是对要修改的元素进行判断。

删除

public E remove(int index) {
        final ReentrantLock lock = this.lock;
        // 1 获取独占锁
        lock.lock();
        try {
            
            // 2 获取对应下标元素
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            
            // 3 确定调整位置,并根据该位置移动元素
            int numMoved = len - index - 1;
            
            // 3.1 要删除的元素在末尾
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            
            // 3.2 要删除的元素在数组中间
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                        numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            // 4 释放锁
            lock.unlock();
        }
    }

删除方法和其它修改方法类似,先是获取独占锁来保证线程安全,接着判断要删除的元素是否是最后一个,如果是最后一个则复制出一个长度-1的新数组然后替换掉旧数组;如果要删除的元素不是最后一个,则分两次复制,随后替换旧数组。

迭代器

/**
    * 迭代器
    *
    * @param <E>
    */
   static final class COWIterator<E> implements ListIterator<E> {
       /**
        * 数据快照
        */
       private final Object[] snapshot;

       /**
        * 遍历数组元素的游标
        */
       private int cursor;

       private COWIterator(Object[] elements, int initialCursor) {
           cursor = initialCursor;
           snapshot = elements;
       }

       /**
        * 判断是否还有下一个元素
        *
        * @return
        */
       public boolean hasNext() {
           return cursor < snapshot.length;
       }

       /**
        * 判断是否有上一个元素
        *
        * @return
        */
       public boolean hasPrevious() {
           return cursor > 0;
       }

       /**
        * 获取下个元素
        *
        * @return
        */
       @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 void remove() {
           throw new UnsupportedOperationException();
       }
       public void set(E e) {
           throw new UnsupportedOperationException();
       }
       public void add(E e) {
           throw new UnsupportedOperationException();
       }
   }

当获取迭代器时,内部会调用 COWIterator 的构造方法,该构造方法有两个参数,一个是 array 数组,另一个是下标 0 。构造方法中会把 array 数组赋值给 snapshot 变量,当其他线程进行了增删改的操作,旧的数组会被新的数组给替换掉,但是 snapshot 还是原来旧的数组的引用。当我们使用迭代器遍历 CopyOnWriteArrayList 的时候,不能保证拿到的数据是最新的,这是弱一致性问题。

设计特点

CopyOnWriteArrayList 仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致。虽然采用了读写分离思想,但写操作时内存里会同时存在两个对象的内存,若这些对象占用内存较大,可能会带来系列问题,如造成频繁 GC。

小结

CopyOnWriteArrayList 是 Java 并发包中相对比较简单的一个实现,它的核心思想是写时复制机制,支持并发读写,但带来的后果是内存 double 和数据一致性问题。