2020年7月21日 作者 zeroheart

CopyOnWriteArrayList

CopyOnWrite技术,主要处理多线程并发访问的问题。普通的ArrayList,如果并发修改,会报一个并发修改异常ConcurrentModificationException。CopyOnWrite则通过在修改数据的时候,进行拷贝数据的方式,解决了这个问题。下面有是一些源码

1.数据结构


/** 可重入锁对象 */
    final transient ReentrantLock lock = new ReentrantLock();

    /** CopyOnWriteArrayList底层由数组实现,volatile修饰 */
    private transient volatile Object[] array;

    /**
     * 得到数组
     */
    final Object[] getArray() {
        return array;
    }

    /**
     * 设置数组
     */
    final void setArray(Object[] a) {
        array = a;
    }

    /**
     * 初始化CopyOnWriteArrayList相当于初始化数组
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

2.add方法

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;

            // 将volatile Object[] array 的指向替换成新数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

3.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;

            // 将array引用指向新数组
            setArray(newElements);
        } else {
            // Not quite a no-op; enssures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

4.get方法

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

final Object[] getArray() {
   return array;
}
  • 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向
  • 写加锁,读不加锁

常用的方法实现我们已经基本了解了,但还是不知道为啥能够在容器遍历的时候对其进行修改而不抛出异常。所以,来看一下他的迭代器吧:

 // 1. 返回的迭代器是COWIterator
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }


    // 2. 迭代器的成员属性
    private final Object[] snapshot;
    private int cursor;

    // 3. 迭代器的构造方法
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    // 4. 迭代器的方法...
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    //.... 可以发现的是,迭代器所有的操作都基于snapshot数组,而snapshot是传递进来的array数组

迭代器操作的是原始对象,所以不会并发修改。

看了上面的实现源码,我们应该也大概能分析出CopyOnWriteArrayList的缺点了。

  • 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据,经常要执行add()、set()、remove()的话,那是比较耗费内存的。
    • 因为我们知道每次`add()、set()、remove()`这些增删改操作都要复制一个数组出来。
  • 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性
    • 从上面的例子也可以看出来,比如线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了(已经调用`setArray()`了)。但是线程A迭代出来的是原有的数据。

CopyOnWriteArraySet的原理就是CopyOnWriteArrayList。

private final CopyOnWriteArrayList al;    
public CopyOnWriteArraySet() {        
al = new CopyOnWriteArrayList();    
}

参考:https://mp.weixin.qq.com/s?__biz=MzI4Njg5MDA5NA==&mid=2247484380&idx=1&sn=c1aa61d29818005f886c61575d2a5d36&chksm=ebd742dddca0cbcb4c8d5792cccfb774a268e64d2b65a94dd0781c17a73bfadf4f930c33f4be&scene=21###wechat_redirect