大家都知道ArrayList是线程不安全的,推荐我们自己加锁或者使用Collections.synchronizedList方法,其实JDK还提供了一种线程安全的List-CopyOnWriteArrayList,它的特征如下:

1、 线程安全
2、 通过锁+数组拷贝+volatile关键字保证了线程安全
3、 每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去

整体思路

从整体讲,CopyOnWriteArrayList数据结构和ArrayList是一致的,底层是个数组

CopyOnWriteArrayList对数组进行操作时,会进行一下操作
1、加锁
2、从原数组中拷贝出新数组
3、在新数组上进行操作,并把新数组赋值给数组容器
4、解锁

另外CopyOnWriteArrayList底层数组采用volatile修饰,保证了可见性

不知道看到这里你是否和我有同样的疑问,既然加锁了,为什么还要加volatile关键字?

读到后面发现CopyOnWriteArrayList读操作并不会加锁,也就是不同线程操作可能拿到不同的锁(或者说有的不拿到锁),也就没法保证happens-before原则,volatile可以保证happens-before语意

新增

新增有很多种情况,比如新增到数组尾部、新增到数组某一个索引位置、批量新增等

首先是新增到数组尾部,源码如下:

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

可以看到,整个过程可以分为五步,加锁、新建数组拷贝原来数组、插入数据、给原来数组赋值、解锁

不知道看到这里你是否和我有同样的疑问,为什么要拷贝一份数据出来呢?

这里其实是和volatile有关,因为volatile修饰的是数组,线程的工作内存只拷贝了数组的引用,如果只改变数组值,无法触发可见性

再看指定位置插入数据

public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }

可以看到,如果插入位置正好是末尾,只需要拷贝一次。否则,会进行两次拷贝

删除

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

可以看得出,删除操作与新增操作类似。分为四步

1、加锁
2、判断索引位置,选择不同策略拷贝数组
3、给原来数组赋值
4、解锁

批量删除

源码如下:

public boolean removeAll(Collection<?> c) {
        if (c == null) throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (len != 0) {
                // temp array holds those elements we know we want to keep
                int newlen = 0;
                Object[] temp = new Object[len];
                for (int i = 0; i < len; ++i) {
                    Object element = elements[i];
                    if (!c.contains(element))
                        temp[newlen++] = element;
                }
                if (newlen != len) {
                    setArray(Arrays.copyOf(temp, newlen));
                    return true;
                }
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

从源码中可以看到,批量删除并不会直接对数组中得到元素挨个删除,而是先对数组中值进行循环判断,把不需要删除的放到临时数组中,最后临时数组中的数据就是我们不需要删除的数据(个人觉得这是一种空间换时间的思想,因为删除操作需要o(n)的时间复杂度)

迭代

CopyOnWriteArrayList,数组原值(包括增加和删除)被改变也不会抛出ConcurrentModificationException异常,因为其迭代器持有的老数组的引用,每次都是拷贝出新数组,不会影响老数组。