CopyOnWriteArrayList应用详解

来源:互联网 发布:360软件管理器下载 编辑:程序博客网 时间:2024/05/21 07:05

CopyOnWriteArrayList介绍

                                                   150342129  魏亚楠

它相当于线程安全的*ArrayList。和ArrayList一样,它是个可变数组*;但是和ArrayList不同的时,它具有以下特性:

  1. 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。

  2. 它是线程安全的

  3. 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。

  4. 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作。

  5. 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。

  6. CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先将原有数组的元素拷贝到新数组中,然后在新的数组中做写操作,写完之后,再将原来的数组引用(volatile 修饰的数组引用)指向新数组。CopyOnWriteArrayList的整个add操作都是在的保护下进行的。

    1. CopyOnWriteArrayList实现了List接口,因此它是一个队列
    2. CopyOnWriteArrayList包含了成员lock。每一个CopyOnWriteArrayList都和一个互斥锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥访问。

    3. CopyOnWriteArrayList包含了成员array数组,这说明CopyOnWriteArrayList本质上通过数组实现的。

java.util.concurrent包中定义常见集合类对应的并发集合类,用于高效处理并发场景,其中CopyOnWriteArrayList对应就是ArrayList。顾名思义CopyOnWrite,写时拷贝,这里写包括对集合类的修改操作,都会创建一个副本。

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景

CopyOnWriteArrayList的基本思想是一旦对“容器”有修改就复制一份新的集合,在新的集合上进行修改,然后将新集合给旧的进行引用。这一部分需要加锁,,然而最大的好处是在“读”操作的时候不需要加锁。

CopyOnWriteArrayList的核心思想是利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的数组,在新的数组上面修改,然后将新数组赋值给旧数组的引用,并通过volatile 保证其可见性,通过Lock保证并发写。

CopyOnWriteArrayList的“动态数组”机制

  • 它内部有个“volatile数组”(array)来保持数据。在“添加(add)/修改(set)/删除(remove)”数据时,都会新建一个数组,并将原数组中的元素拷贝到新数组中,然后在新数组中更新,最后再将“volatile数组”引用指向新数组。这就是它叫做CopyOnWriteArrayList的原因!CopyOnWriteArrayList就是通过这种方式实现的动态数组;不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很低;但是单单只是进行遍历查找的话,效率比较高。
  • CopyOnWriteArrayList的“线程安全”机制

    • 是通过 volatile互斥锁 来实现的。
    • CopyOnWriteArrayList是通过“volatile数组”来保存数据的。一个线程读取volatile数组时,总能看到其它线程对该volatile数组最后的写入;就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的保证。
    • CopyOnWriteArrayList通过互斥锁来保护数据。在“添加/修改/删除”数据时,会先“获取互斥锁”,再修改完毕之后,先将“volatile数组”引用指向新数组,然后再“释放互斥锁”;这样,就达到了保护数据的目的。

    CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其中所有可变操作(例如add,set等)都是通过对底层数组进行一次新的复制来实现的。这一般是需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量是,这种方法可能比其他替代方法更有效。在不能或不想进行同步遍历,但是又从并发线程中排除冲突时,是非常有用的。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出ConcurrentModificationException。创建迭代器以后,迭代器不会反映列表的添加,移除或者修改。在迭代器上进行的元素更改操作(例如remove,set和add)不受支持。这些方法将抛出UnsupportedOperationException。这个类比较简单,在一些可变操作下通过加锁并对底层数组进行一次复制来实现。

    类的定义

    public class CopyOnWriteArrayList<e>    implements List<e>, RandomAccess, Cloneable, java.io.Serializable</e></e>

    由此可以看出没有继承子类,实现接口和ArrayList一样。
    关键属性
    /** 锁保护所有的应用程序 */transient final ReentrantLock lock = new ReentrantLock();/** 数组使用private修饰限制访问,数组只能通过getarray/setarray访问,数组使用volatile修饰保证可见性,不读缓存直接读写内存使用transient修饰,表示序列化时忽略此字段(自己定制序列化操作) */private volatile transient Object[] array;
    采用数组方式实现,多了一个volatile声明,用于保证线程可见性。个线程对array的修改对另一个线程可见,但不是立即可见。没有size声明表示实际包含元素的大小,多了一个ReentrantLock对象声明。

    CopyOnWriteArrayList构造函数

    底层数据结构

    //初始对象构造一个对象为0的list

    public CopyOnWriteArrayList() { //采用Object数组存储数据        setArray(new Object[0]);}final void setArray(Object[] a) {        array = a;}

    定制序列化操作
    因为Object数组被transient修饰,因此需要CopyOnWriteArrayList类自己制定序列化方案
    方法writeObject和readObject处理对象的序列化。如果声明该方法,它将会被ObjectOutputStream调用而不是采用默认的序列化方案。ObjectOutputStream使用了反射来寻找是否声明了这两个方法。因为ObjectOutputStream使用getPrivateMethod,所以这些方法不得不被声明为priate以至于供ObjectOutputStream来使用。
    在两个方法的开始处,调用了defaultWriteObject()和defaultReadObject()。它们做的是默认的序列化进程,就像写/读所有的non-transient和 non-static字段(但他们不会去做serialVersionUID的检查).通常说来,所有我们想要自己处理的字段都应该声明为transient。这样的话,defaultWriteObject/defaultReadObject便可以专注于其余字段,而我们则可为被transient修饰的字段定制序列化。
    /**     保存列表的状态到一个流,也就是序列化     */    private void writeObject(java.io.ObjectOutputStream s)        throws java.io.IOException{        s.defaultWriteObject();        Object[] elements = getArray();        // 写出数组的长度        s.writeInt(elements.length);        //按顺序写出所有的元素        for (Object element : elements)            s.writeObject(element);    }
    先调用s.defaultWriteObject()对非transient修饰的字段进行序列化操作
    然后序列化写入数组的长度,再循环写入数组的元素
    /**     反序列化     */    private void readObject(java.io.ObjectInputStream s)        throws java.io.IOException, ClassNotFoundException {        s.defaultReadObject();        // 绑定到新锁        resetLock();        // Read in array length and allocate array        int len = s.readInt();        Object[] elements = new Object[len];        // 按照顺序读出所有的元素        for (int i = 0; i < len; i++)            elements[i] = s.readObject();        setArray(elements);    }

    反序列化的时候先调用s.defaultReadObject()恢复没有被transient修饰的字段
    然后为反序列化得到的CopyOnWriteArrayList对象创建一把新锁
    接着恢复数组的长度,根据数组的长度创建一个Object的数组
    然后恢复数组的每一个元素

     构造方法

    public CopyOnWriteArrayList() {    setArray(new Object[0]); //默认创建一个空数组}public CopyOnWriteArrayList(Collection<!--? extends E--> c) {    Object[] elements = c.toArray();        if (elements.getClass() != Object[].class)        elements = Arrays.copyOf(elements, elements.length, Object[].class);//拷贝一份数组    setArray(elements);}

    size方法,直接返回数组大小,说明array数组只包含实际大小的空间

    public int size() {        return getArray().length;}

    get方法,和ArrayList中类似,不过没有index的范围判断,读操作是直接通过getArray方法获取Object数组,然后通过下标index直接访问数据。读操作并没有加锁,也没有并发的带来的问题,因为写操作是加锁写数组的副本,写操作成功将副本替换为原数据,这也是写时复制名字的由来

    public E get(int index) {        return (E)(getArray()[index]);}
  • private E get(Object[] a, int index) {        return (E) a[index];    }
  • 加锁写副本

  • public E set(int index, E element) {        final ReentrantLock lock = this.lock;        lock.lock();        try {            Object[] elements = getArray();            E oldValue = get(elements, index);            if (oldValue != element) {                int len = elements.length;                Object[] newElements = Arrays.copyOf(elements, len);                newElements[index] = element;                setArray(newElements);            } else {                // Not quite a no-op; ensures volatile write semantics                setArray(elements);            }            return oldValue;        } finally {            lock.unlock();        }    }

    set方法先通过lock加锁,然后获取index位置的旧数据,供最后方法返回使用

    E oldValue = get(elements, index);

    创建数组的副本,在副本上进行数据的替换

    Object[] newElements = Arrays.copyOf(elements, len);

    Arrays.copyOf(elements, len)方法将会从elements数组复制len个数据创建一个新的数组返回
    然后在新数组上进行数据替换,然后将新数组设置为CopyOnWriteArrayList的底层数组

    newElements[index] = element;  setArray(newElements);

    最后在finally块里边释放锁
    特定位置添加数据,添加数据和替换数据类似,先加锁,然后数组下标检查,接着创建数组副本,在副本里边添加数据,将副本设置为CopyOnWriteArrayList的底层数组

    add方法,可以看到无论是在尾部还是指定位置添加,都有锁定和解锁操作,在设置值之前都先将原先数组拷贝一份并扩容至size+1大小。add()方法的实现很简单,通过加锁保证线程安全,通过Arrays.copyOf根据原数组复制一个新的数组,将要插入的元素插入到新的数组的对应位置,然后将新的数组赋值给array,通过volatile保证内存可见。


    //在列表的指定位置插入指定的元素//将当前处于该位置的元素移动和任何后续//添加一个索引public void add(int index, E element) {    // 加锁    final ReentrantLock lock = this.lock;    lock.lock();    try {        // 读取底层数组对象        Object[] elements = getArray();        int len = elements.length;        if (index > len || index < 0)            throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + len);        Object[] newElements;        // 由于数组的长度不可变,所以插入一个元素需要新建一个新的数组以容纳新插入的元素        // 所以需要将原数组复制到新的数组        int numMoved = len - index;// 需要移动的元素的开始位置(要插入位置的下一个位置)        if (numMoved == 0) // ==0表示插入的位置是数组的最后一个位置,所以该位置前面的元素原样不动复制到新的数组即可            // 这里通过复制elements数组生成一个新的数组,注意这里新的数组长度是原数组+1,所以新数组的最后一个元素是NULL            newElements = Arrays.copyOf(elements, len + 1);        else {            // 将原数组的0~index-1原样复制到新的数组中,            // 而index之后的元素对应复制到新数组的index+1之后,即中间空出一个位置用于放置带插入元素            newElements = new Object[len + 1];            System.arraycopy(elements, 0, newElements, 0, index);            System.arraycopy(elements, index, newElements, index + 1, numMoved);        }        // 将element插入到新的数组        newElements[index] = element;        // 将更新底层数组的引用,由于array是volatile的,所以对其的修改能够立即被后续线程可见        setArray(newElements);    } finally {        // 释放锁        lock.unlock();    }}//结束指定的元素列表,把元素添加到指定列表// 该方法相当于调用add(array.length, e)//添加元素    public boolean add(E e) {        //获取该对象的锁        final ReentrantLock lock = this.lock;        // 获取“锁”,每次只有一个线程可进入临界区        lock.lock();        try {            // 获取原始”volatile数组“中的数据和数据长度。            Object[] elements = getArray();            int len = elements.length;            // 新建一个数组newElements,并将原始数据拷贝到newElements中;            // newElements数组的长度=“原始数组的长度”+1            Object[] newElements = Arrays.copyOf(elements, len + 1);            // 将“新增加的元素”保存到newElements中。            newElements[len] = e;            // 将”volatile数组“引用指向newElements数组,这样旧数组就被GC回收了            setArray(newElements);            return true;        } finally {            // 释放“锁”            lock.unlock();        }    }set方法,ArrayList中set方法直接改变数组中对应的引用,这里需要拷贝数组然后再设置。set()比add()更新简单,只需要复制一个新的数组,然后更新新的数组的指定位置的元素,然后更新引用即可。//将此列表中的指定位置的元素用指定的元素替换public E set(int index, E element) {    // 加锁    final ReentrantLock lock = this.lock;    lock.lock();    try {        Object[] elements = getArray();        // 获取需更新的元素        Object oldValue = elements[index];        // //        // 需更新的值不等于原值(注意此处的不等是==,不是equals(),即oldValue和element必须是引用同一个对象才可)        if (oldValue != element) {            int len = elements.length;            // 复制一个新的数组,并将index更新成新的值,更新引用            Object[] newElements = Arrays.copyOf(elements, len);            newElements[index] = element;            setArray(newElements);        } else {            // Not quite a no-op; ensures volatile write semantics            // 此处由于更新的值与原值是同一个对象,所以其实可不更新引用            // 从注释可以看出更新的目的是出于写volatile变量            setArray(elements);        }        return (E) oldValue;    } finally {        // 释放锁        lock.unlock();    }}remove(int)方法,和指定位置添加类似,需要拷贝[0,index)和[index+1,len)之间的元素//删除索引index处的元素public E remove(int index) {    // 加锁    final ReentrantLock lock = this.lock;    lock.lock();    try {        Object[] elements = getArray();        int len = elements.length;        Object oldValue = elements[index];        int numMoved = len - index - 1;// 需要移动的元素的个数        if (numMoved == 0) // ==0表示删除的位置是数组的最后一个元素,只需要简单的复制原数组的len-1个元素到新数组即可            setArray(Arrays.copyOf(elements, len - 1));        else {            // 将原数组的0-index-1复制到新数组的对应位置            // 将原数组的index+1之后的元素复制到新数组,丢弃原数组的index位置的元素            Object[] newElements = new Object[len - 1];            System.arraycopy(elements, 0, newElements, 0, index);            System.arraycopy(elements, index + 1, newElements, index, numMoved);            setArray(newElements);        }        return (E) oldValue;    } finally {        lock.unlock();    }}remove(Object)方法,分配一个len-1大小的新数组,遍历原来数组,如果找到则将原来数组以后的元素拷贝到新数组中并将list设置为新数组,否则直接给新数组赋值上原来数组。public boolean remove(Object o) {    // 加锁    final ReentrantLock lock = this.lock;    lock.lock();    try {        Object[] elements = getArray();        int len = elements.length;        if (len != 0) {            // Copy while searching for element to remove            // This wins in the normal case of element being present            int newlen = len - 1;// 删除之后数组的长度            Object[] newElements = new Object[newlen];// 创建新的数组            for (int i = 0; i < newlen; ++i) {// 从0-len-1遍历原数组                if (eq(o, elements[i])) {// 如果是待删除元素,则将该元素之后的元素复制到新数组中                    // found one; copy remaining and exit                    for (int k = i + 1; k < len; ++k)                        newElements[k - 1] = elements[k];                    // 设置新数组                    setArray(newElements);                    return true;                } else                    // 将该元素插入到新数组                    newElements[i] = elements[i];            }            // 确认最后原数组一个元素是否与待删除元素相等,是的话直接将修改引用即可,因为前面已经为新数组赋完值了            // special handling for last cell            if (eq(o, elements[newlen])) {                setArray(newElements);                return true;            }        }        // 到这里说明数组中没有与待删除元素相等的元素,所以直接返回false,        // 但是这里并没有写volatile变量,看来set那里也只是写着好玩        return false;    } finally {        lock.unlock();    }}


    迭代器的实现

        ArrayList中迭代器支持fastfail,一旦检测到遍历过程中发送了修改则会抛出ConcurrentModificationException;CopyOnWriteArrayList的迭代器由于修改的时候都会重新copy一份数组,因此不存在并发修改问题,也不会抛出ConcurrentModificationException。同样支持单向和双向迭代器,其iterator和listIterator方法都是通过内部类COWIterator创建,只是前者返回接口限定为单向迭代Iterator。


    COWIterator定义:

    /** 数组快照**/private final Object[] snapshot;/** 随后调用下一个元素返回元素的索引  */private int cursor;构造器private COWIterator(Object[] elements, int initialCursor) {            cursor = initialCursor;            snapshot = elements;}iterator和listIterator中会传递当前数组的引用和cursor(无参方法为0,有参数方法为对应值)public boolean hasNext() {    return cursor < snapshot.length;}public boolean hasPrevious() {    return cursor > 0;}public E next() {    if (! hasNext())        throw new NoSuchElementException();    return (E) snapshot[cursor++];}public E previous() {    if (! hasPrevious())        throw new NoSuchElementException();    return (E) snapshot[--cursor];}

    CopyOnWriteArrayList的缺点:
               从CopyOnWriteArrayList的实现原理可以看到因为在需要容器对象的时候进行拷贝,所以存在两个问题:内存占用问题和数据一致性问题内存占用问题

    因为需要将原来的对象进行拷贝,这需要一定的开销。特别是当容器对象过大的时候,因为拷贝而占用的内存将增加一倍(原来驻留在内存的对象仍然在使用,拷贝之后就有两份对象在内存中,所以增加了一倍内存)。而且,在高并发的场景下,因为每个线程都拷贝一份对象在内存中,这种情况体现得更明显。由于JVM的优化机制,将会触发频繁的Young GC和Full GC,从而使整个系统的性能下降。
    数据一致性问题

    CopyOnWriteArrayList不能保证实时一致性,因为读线程在将引用重新指向原来的对象之前再次读到的数据是旧的,所以CopyOnWriteArrayList只能保证最终一致性。因此在需要实时一致性的场景CopyOnWriteArrayList是不能使用的。
    CopyOnWriteArrayList小结

        CopyOnWriteArrayList适用于读多写少的场景,通过空间换时间的方式来提高读的效率并保证写的安全性
        在并发操作容器对象时不会抛出ConcurrentModificationException,并且返回的元素与迭代器创建时的元素是一致的
        容器对象的复制需要一定的开销,如果对象占用内存过大,可能造成频繁的YoungGC和Full GC
        CopyOnWriteArrayList不能保证数据实时一致性,只能保证最终一致性
        CopyOnWriteArrayList的迭代器保留一个执行底层基础数组的引用,这个数组当前位于迭代器的起始位置,由于基础数组不会被修改(修改都是复制一个新的数组),因此对其同步只需要保证数组内容的可见性。多个线程可以同时对这个容器进行迭代,而不会彼此干扰或者与修改容器的线程互相干扰。不会抛出CocurrentModificationException,并且返回元素与创建迭代器创建时的元素完全一致,不必考虑之后修改操作带来影响。
        每次修改容器都会复制底层数组,这需要一定开销,特别是容器规模较大。仅当迭代操作远远多于修改操作时,才应该使用CopyOnWriteArrayList

  •   
  • 原创粉丝点击