CopyOnWriteArrayList源码与原理分析

1、CopyOnWriteArrayList简介

       CopyOnWriteArrayList是线程安全的ArrayList,在读多写少的高并发场景中性能卓越,数据一致性遵循base原则,弱一致性,确保最终一致性;为了加深理解,我们先来梳理下ArrayList发展历史上的实现对比吧;

       ArrayList,线程不安全的,在并发环境下,在写线程在写数据的时候,基于fast-fail机制,会排除并发操作异常;

       Vector/Collections的静态方法获取的ArrayList,是线程安全的类,使用synchronzied进行修饰,独占锁方式保证线程安全性,因此执行效率不高;

       ReenTrantReadWriteLock进行控制,读锁和写锁,满足读写分离的思想,读读之间不会形成阻塞,在读多写少的场景中性能得到了大大的提高;但是读线程还是会因为写操作被阻塞,因此才有了CopyOnWriteArrayList;

       CopyOnWriteArrayList,可以保证线程安全,采用cow设计思想,在写操作的时候不会阻塞读线程,因此读线程效率得到了进一步的提升;单其缺点是,弱一致性和在增删改的时候内存开销大较大容易引起GC;

      【补充】:ArrayList为什么查询快,增删慢?

        ArrayList底层采用数组实现,在查询的时候根据数组下标可以快速的找到对应元素,修改元素也是如;

        在删除和增加的时候因为要涉及到数组的移动,因此会比较慢;

 

2、基本API及原理

基本Api

/**增加 add*/
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  //进行数组扩容,不够就扩容
    elementData[size++] = e;
    return true;
}

public void add(int index, E element) {
    rangeCheckForAdd(index); //检查是否越界

    ensureCapacityInternal(size + 1);   //进行数组容量判断,不够就扩容
    //将index至数据最后一个元素整体往后移动一格,然后插入新的元素
    System.arraycopy(elementData, index, elementData, index + 1, 
                     size - index);
    elementData[index] = element;
    size++;
}




/**删除*/
public E remove(int index) {
    rangeCheck(index); //判断是否越界

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0) //若该元素不是最后一个元素的话,将index+1至数组最后一个元素整体向前移动一格
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}


public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}




/**修改*/
public E set(int index, E e) {
    rangeCheck(index);
    checkForComodification();
    E oldValue = ArrayList.this.elementData(offset + index);
    ArrayList.this.elementData[offset + index] = e; //——将数组对应的index的元素进行替换
    return oldValue;
}






/**查询*/
public E get(int index) {
    rangeCheck(index); //数组越界判断
    checkForComodification();
    return ArrayList.this.elementData(offset + index); //获取数组对应的index元素
}

E elementData(int index) {
    return (E) elementData[index];
}

数据结构及原理

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
}

     1、实现了List接口,因此其拥有List接口的所有功能;

     2、内部持有一个ReentrantLock lock=new ReentrantLock(),内部的并发操作,通过他来进行支持并发操作;

     3、底层是用volatile transient声明地数组 array保存数据;volatile确保array的可见性,是数组引用的可见性,当数组被修改的时候是并不会确保可见性的;  可参考:volatile是否能保证数组中元素的可见性?

---->问题来了?这和我们文章开头分析的读写锁不是一样嘛?接下来就是最重要的第四点Cow(Copy-On-Write)思想--

    4、当有写操作的时候,复制一个新的数组出来,完成插入,修改,或移除操作之后将新的数组复制给array;这样在赋值之外我们的老array是不会进行增删改的操作的;放弃数据的实时性达到最终一致性;

 

3、源码分析

get():  这里的方法居然和我们普通的get毫无区别?   是不是很惊奇!!!!

        我们在底层对array进行了可见性的修饰,因此一旦改变我们获取的就是最新的;而增删改操作也不会在这里进行,因此我们获取的时候直接get() 就好了;  这就是CopyOnWriteArrayList的get效率超好的原因所在

        但其实我们是用空间换时间,oh,空间换时间是什么意思?   稍后add方法我们就会详细讲解啦。

//获取数组,然后获取数组指定下标的值
public E get(int index) {
    return get(getArray(), index);
}

final Object[] getArray() {
    return array;
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

 

add()  : lock的经典操作,确保数据在写操作的时候只有一个线程在进行;   

           Arrays.copyOf(elements, len + 1);  这里居然创建了原来数组大小 +1 的数组,也就是说,这里会使用原来数组的2份的大小进行操作;

    ----->从这里我们就可以看到CopyOnWriteArrayList的缺点了-------

          缺点:1、在写操作的时候内存量会上升,在进行频繁的add操作的时候,或者在对象占用内存比较大的时候,会造成频繁的minor GC 和major GC;

                    2、我们的数据一致性采用的是最终一致性,我们数据更新并不会实时马上被其他线程读取到;

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    //1. 使用Lock,保证写线程在同一时刻只有一个
    lock.lock();
    try {
        //2. 获取旧数组引用
        Object[] elements = getArray();
        int len = elements.length;
        //3. 创建新的数组,并将旧数组的数据复制到新数组中
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //4. 往新数组中添加新的数据            
        newElements[len] = e;
        //5. 将旧数组引用指向新的数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

 

set()

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

 

remove()

   public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0)   //数组值删除完了的情况
                setArray(Arrays.copyOf(elements, 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的实现:  在这里我们在获取迭代器的时候,获取的是快照array,并不是最新的正在修改的;

//获取迭代器
public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);  //获取当前指向的array给迭代器构造函数;
}



static final class COWIterator<E> implements ListIterator<E> {
    private final Object[] snapshot; //array的快照版本
    private int cursor; //数组下标

    private COWIterator(Object[] elements, int initialCursor) { //构造函数snapshot 指向弱一致性的array
        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++];
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值