CopyOnWriteArrayList使用及源码分析

一、demo

public class CopyOnWriteArrayListDemo {
    
    public static void main(String[] args) throws Exception{
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
        list.add("a1");
        System.out.println(list);
    }
}

二、构造方法源码分析

从下面这段构造函数的代码,就可以看出来,CopyOnWriteArrayList其实也是底层基于数组来实现的,volatile修饰,保证多线程读写的可见性,只要有一个线程修改了这个数组,其他的线程立马是可以读到的;

public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
private transient volatile Object[] array;

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

三、add方法源码分析

final transient ReentrantLock lock = new ReentrantLock();

public boolean add(E e) {
  	/** 通过ReentrantLock进行加锁,保证同一时间只能有一个线程来操作底层的数组数据结构
    	* 其实就可以判断更新CopyOnWriteArrayList的时候
      * 肯定都会通过ReentrantLock进行加锁来保证多线程并发安全的
    	* 加锁原理不清楚的可以回过头看看我之前分析的ReentrantLock源码分析
      */
    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;
      	/** 将新数组设置到CopyOnWriteArrayList里持有的数组里面去 */
        setArray(newElements);
        return true;
    } finally {
      	/** 锁释放 */
        lock.unlock();
    }
}

四、set方法源码分析

public E set(int index, E element) {
  	/** 获取ReentrantLock锁进行加锁 */
    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) {
  	/** 获取ReentrantLock锁进行加锁 */
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
      	/** 获取数组,并获取指定下标中的数组元素 */
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
      	/** 判断第一段拷贝数组数量 如:len = 5,index=2 numMoved=5-2-1=2 */
        int numMoved = len - index - 1;
      	/** 判断删除元素是否为最后一位
        	* 如果是则将老数组元素赋值给新数组,忽略最后一位元素不进行赋值
          * 新数组大小为:老数组大小-1
          */
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
          	/** 构建新数组,因为是删除元素,所以新数组大小为老数组长度-1 */
            Object[] newElements = new Object[len - 1];
          	/** 把老数组里的,从index = 0开始的元素,截止到index = 2的元素
            	* 复制到新数组的从index = 0位置开始的地方
              * 复制的数据为index = 0、index = 1
              */
            System.arraycopy(elements, 0, newElements, 0, index);
          	/** index = 2,从老数组的index = 3,拷贝2个元素
            	* index = 3和index = 4的两个元素给拷贝到了新数组里去
              * 从新数组的index = 2的位置开始放置
              */
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
          	/** 将新数组赋值给CopyOnWriteArrayList中的数组变量 */
            setArray(newElements);
        }
        return oldValue;
    } finally {
      	/** 释放锁 */
        lock.unlock();
    }
}

六、get方法源码分析

直接从底层数组里来读取数据,通过index定位对应位置的元素,这个是不加锁的;

因为写数据的时候,会复制一个副本,新的数组,对新的数组来修改,修改好了设置回去就可以了。所以在写数据的同时读数据其实不是读的同一份数据,所以读和写之间是没有锁的冲突的,读只有两种情况:

第一种:我读到的老数组的数据;

第二种:其他线程更新好了数组,volatile写,我读到的是新数组的数据;


public E get(int index) {
  	/** 获取数组,在根据index获取数组中的元素 */
    return get(getArray(), index);
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

七、iterator方法源码分析

iterator其实是拷贝了一个数组副本,基于这个副本进行迭代的,如果在这个时期CopyOnWriteArrayList添加了元素迭代线程是感知不到的,会继续将之前数组里面元素迭代出来;

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}
/** 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 E next() {
    if (! hasNext())
        throw new NoSuchElementException();
    return (E) snapshot[cursor++];
}

总结

缺点

弱一致性或者最终一致性:多个线程并发的读写list,写的时候是需要一段时间,写好之前此时其他线程读到的都是老数组的数据,这个过程中,多个线程看到的数据是不一致的,人家修改了数据没有立马被人读到;

空间换时间:写的时候,经常内存里会出现复制出来的一模一样的副本,对内存消耗过大;

优点

读和写不互斥的,写和写互斥,同一时间就一个人可以写,但是写的同时可以允许其他所有人来读;读和读也是并发的;读写锁机制还要好;

原文链接:,转发请注明来源!