CopyOnWriteArrayList的实现

CopyOnWriteArrayList是自JDK5开始提供的支持并发读写的List。从名字可以大致的看出来其内部的设计思想:在写入前copy一份数据以提供修改。下面看一下具体的实现:

基本结构

    final transient ReentrantLock lock = new ReentrantLock();
    private transient volatile Object[] array;

CopyOnWriteArrayList内部有两个属性:lock表示一把锁,在写入的时候使用;另外一个用于数据存储的array[], 这里是volatile类型的,意思就是当新的array替换时,可以立即生效(多线程可见)。

常用方法

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;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

上面代码可以很容易理解:加锁、拷贝数组、释放锁。add完成后,array[]容量+1。

set()

    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();// 加锁
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);// 获取oldValue

            if (oldValue != element) {// 新旧值不同时,替换
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // 确保volatile写语义(保证element的可见性)
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();// 释放锁
        }
    }

上面代码:加锁、拷贝数组、释放锁。整体流程没什么问题,但是有一点需要说一下的就是:当element和oldValue引用相同时,还是进行了一下setArray()操作。这是因为element如果在外部被改变,其他线程并不能立即知道(因为volatile关键字修饰的是array[])。当调用setArray()重新赋值时,会使array[]整体对其他线程可见。

get()

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

从上面代码可以看出,get()过程中没有任何的锁操作。所以获取到的值并不能保证实时性。

总结

CopyOnWriteArrayList使用时需要考虑的两个问题是:1、数组拷贝时的内存占用问题;2、数据一致性问题。从上面的方法可以看出来,CopyOnWriteArrayList适合读远大于写的场景。