简介

  我们都很熟悉容器对象ArrayList,并且在初学时就被告知ArrayList不是线程安全的:当我们在使用迭代器遍历ArrayList时,如果有其他线程修改了ArrayList对象,那么就会抛出ConcurrentModificationException异常。相较于Vector使用synchronized加锁保证线程安全性,JUC提供了多线程版“ArrayList”:CopyOnWriteArrayList。下面是JDK对CopyOnWriteArrayList的介绍:

大意是CopyOnWriteArrayList是线程安全版本的ArrayList,所有对CopyOnWriteArrayList修改的操作都是在内部数组的拷贝上进行操作的,这样做虽然内存花费大,但是在遍历操作大于修改操作时这样效率更高,可以有效防止抛出ConcurrentModificationException异常,迭代器迭代期间不支持对元素更改操作,否则会抛出UnsupportedOperationException异常。

类结构

  CopyOnWriteArrayList实现了List接口,List表示是有序的Collection,即它用某种特定的插入顺序来维护元素顺序;实现了标记接口RandomAccess接口支持快速访问;实现了Iterable接口可以使用迭代器遍历容器元素。

多线程十之CopyOnWriteArrayList源码分析-LMLPHP

源码解析

构造方法

  CopyOnWriteArrayList互斥锁用于对修改容器元素阶段加锁,被volatile修饰的Object数组是CopyOnWriteArrayList存储数据的底层数据结构,通过volatile保证能够读到其他线程对CopyOnWriteArrayList数据的修改,对于数组的访问都是通过getArray/setArray方法。
  CopyOnWriteArrayList提供了三个重载的构造函数,无参构造函数会调用setArray方法构造一个空的Object数组,另外两个构造函数分别传入集合/数组参数,将集合/数组内元素存入CopyOnWriteArrayList的底层Object数组。

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

    //互斥锁
    final transient ReentrantLock lock = new ReentrantLock();

    //底层存储数据数组,只能通过getArray/setArray访问设置,volatile动态数组
    private transient volatile Object[] array;

    final Object[] getArray() {
        return array;
    }

    final void setArray(Object[] a) {
        array = a;
    }

    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

    //传入Collection集合对象,将集合中元素存入CopyOnWriteArrayList
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }

    //传入数组
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
}

add(E e)

   add方法的作用是把传入元素添加到链表list的末尾。add方法有两点需要注意:1.在写入过程使用了互斥锁,所以同一时间只有一个线程在修改CopyOnWriteArrayList 2.增加元素并不是直接在原数组操作,而是在原数组的拷贝数组上添加元素的,添加完成后再调用setArray方法用新数组代替原始数组

    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(int index, E element)

  这个方法的作用是把新元素插入到特定位置,会把原来位置的元素向后挤。过程与上面的add大致相同。

    public void add(int index, E element) {
        //互斥锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //原始数组
            Object[] elements = getArray();
            int len = elements.length;
            //检查index有效性
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            //拷贝数组
            Object[] newElements;
            //从index到数组末尾要向后移动一位数组元素的个数
            int numMoved = len - index;
            //如果index==length,直接把原数组复制到新数组
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            //否则分成两段复制,原始数组index前面的元素位置一一对应赋值到新数组,原数组index开始的元素复制到
            //新数组index+1到length+1,相当于依次后移。空出来的index就是新元素插入的位置
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            //插入新元素
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }

get(int index)

  获取索引位置为index位置处的元素,获取元素过程中没有使用互斥锁上锁。

    public E get(int index) {
        return get(getArray(), index);
    }

    @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        //返回数组index处位置
        return (E) a[index];
    }

remove(int index)

  移除index处元素。由于涉及到修改到对链表内元素的修改,因此移除过程会使用互斥锁上锁。

    public E remove(int index) {
        //上锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //原始数组
            Object[] elements = getArray();
            int len = elements.length;
            //数组index处要移除的元素
            E oldValue = get(elements, index);
            //index+1到数组末尾要移动的元素个数
            int numMoved = len - index - 1;
            //如果要移除的元素在数组末尾(index=len-1),直接复制数组区间[0,len-2]所有元素到新数组
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            //如果移除的元素不再末尾,分成两段赋值,首先把[0,index-1]区间元素复制到新数组,再把
            //[index+1,len-1]复制到新数组
            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 {
            lock.unlock();
        }
    }

迭代器Iterator遍历

  CopyOnWriteArrayList支持使用迭代器迭代,使用iterator方法返回COWIterator对象,在迭代过程中没有上锁,也不支持remove/set/add等修改方法。

    //返回COWIterator对象
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

    //实现迭代器的内部类
    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;
        }

        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;
        }
    }

总结

  Copy-On-Write简称COW,中文简称写入时复制,是一种程序设计优化策略,具体思想就是对于共享内容做修改操作时,会把共享内容复制出来,在复制内容上修改,修改完成后在返还到原内容上。对于CopyOnWriteArrayList而言,向容器添加元素是先把容器复制一份,向复制的容器添加元素,添加成功后把复制的容器赋值给原容器对象;而对于读取容器的操作直接在原容器进行操作。CopyOnWriteArrayList利用了COW技术实现读写的分离,对于写操作实行加锁保证安全性,读操作不改变容器不需加锁,相对于Vector对所有操作加锁来保证安全性的效率更高,适合于读多写少的场景。同时在CopyOnWriteArrayList保存数据量较大时,对于容器的写入由于复制原容器产生新容器用于写操作,造成了两倍的内存消耗,会引发频繁的垃圾回收,降低性能。

03-25 13:51