CopyOnWriteArrayList源码简析

CopyOnWriteArrayList,是在concurrent包下的一个类,说明这是一个并发安全的类,从命名来看,这是一个线程安全的ArrayList,CopyOnWrite则是实现他并发安全的机制,即写时复制

CopyOnWriteArrayList类的用法和ArrayList基本是一样的,所以就不写demo了,这里直接来看一下add方法的源码吧

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 获取原数组
        Object[] elements = getArray();
        int len = elements.length;
        // 生成一个新的数组,且数组长度 + 1,将老数组复制到新数组中
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 将新加的元素放在数组尾部
        newElements[len] = e;
        // 重新设置数组为新数组
        setArray(newElements);
        return true;
    } finally {
    	// 释放锁
        lock.unlock();
    }
}

这里实现并发安全的方法很简单,就是加了一个ReentrantLock,我们知道list的底层其实就是数组,这里在操作list的时候,每次都是创建了一个新的数组,将旧数组的值复制到新数组,然后再将要添加的元素加入新数组中
其实CopyOnWriteArrayList类中的增删改都是这个套路,先加锁,然后再使用旧数组复制出来一个新数组,进行各种的增删改操作

看到这里,相信大家应该有点理解写时复制机制的意思了,无非就是在进行写操作的时候,复制一个新的数组出来,将写操作之后的结果放入到新数组之后然后返回,那为什么要这么折腾呢?

结果就隐藏在读操作中,下面再来看看get方法的源码

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

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

get方法的源码其实就是最简单的从数组中获取我们想要位置的元素,没有任何加锁,或是CAS等操作,那自然效率很高了,get方法能够什么都不做还能保证并发安全性就要归功于写时复制机制了

private transient volatile Object[] array;

这里补充一下CopyOnWriteArrayList操作的数组的变量,array是使用了volatile关键字修饰的,所以他可以保证可见性

好,现在我们假设一个add操作和get操作并发来执行了,这里会存在两种情况
第一种:add操作对新数组的操作还没有完成,此时CopyOnWriteArrayList中的数组还是老数组,此时读操作就基于老数组完成
第二种:add操作执行完成,同时CopyOnWriteArrayList的新数组也设置完毕了,volatile关键字能保证数组的可见性,立马就能被读操作的线程访问到

这两种情况下,写操作和读操作是不会发生并发问题的,因为写操作一直是基于数组副本来增删改,读操作则一直是读的数组的array,这就是为什么要写时复制了,这种做法是典型的空间换时间
通过上面的分析,我们也可以知道CopyOnWriteArrayList的使用场景了,写操作的时候,需要加锁,同时复制数组副本进行操作,而在读操作的时候,不需要做其他的并发安全处理,所以CopyOnWriteArrayList非常适合读多写少的场景

既然CopyOnWriteArrayList是并发安全的,那么是不是我们所做的所有操作都可以保证并发安全性呢?
在上面我们说读操作的时候,可能会出现两种情况,一种情况是读旧数组,一种是读新数组,所以在一小段时间内,不同的线程读操作可能会读到不同的值,产生弱一致性的问题
还有另外一个场景,在CopyOnWriteArrayList迭代的时候,这里贴出一部分源码

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

static final class COWIterator<E> implements ListIterator<E> {
    /** 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快照使用的是当前的数组对象
        snapshot = elements;
    }
    //省略源码
}

从源码中可以看出在迭代的时候,new了一个COWIterator对象,里面的snapshot快照使用的当前的数组,之后的迭代操作都是基于这个snapshot来做的,也就是说如果此时有别的线程来进行写操作的话,iterator方法里面使用的还是旧数组,所以不会产生并发安全问题,但是迭代出的数据也不一定就是最新的数据,如果你非要保证数据的强一致性的话,还是需要通过加锁来保证

CopyOnWriteArrayList还有一个问题,之前提到了一下,就是空间换时间,如果你要操作的list非常大的话,多个线程操作数组副本的话,可能会消耗大量的内存空间,这也是需要注意的一个点

在使用集合包下面的并发工具类的时候,一定要做到对各个类的特性心中有数,这样才能根据不同的业务场景选择我们所需要的工具,做到游刃有余

你可能感兴趣的:(并发编程)