CopyOnWriteArrayList学习笔记

前言

并发包中的并发List只有CopyOnWriteArrayList。CopyOnArrayList是一个线程安全的ArrayList,对其进行修改的操作都是在底层的一个复制的数组上进行的,也就是使用了写时复制策略。

CopyOnWriteArrayList源码解析

初始化
public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

无参构造函数内部创建了一个大小为0的Object数组作为array的初始值。然后看下有参构造函数:

// 如果是普通的 list ,都会重新拷贝一份,不会影响原来的 list
public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }
//创建list,其内部元素是入参toCopyIn的副本
public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

添加元素

尾部添加

public boolean add(E e) {
    	//(1)获取独占锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            // (2)得到所有的原数组
            Object[] elements = getArray();
            int len = elements.length;
            //(3)拷贝到新数组里面,新数组的长度是 + 1 的
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //在新数组中进行赋值,新元素直接放在数组的尾部
            newElements[len] = e;
            //(4)替换原来的数组
            setArray(newElements);
            return true;
        } finally {
            //(5)释放独占锁
            lock.unlock();
        }
    }

从上get到:

  1. 调用add方法只有一个线程会获取到该锁,其它线程会被阻塞挂起直到锁被释放
  2. 新数组的大小是原来数组大小增加1,所以CopyOnWriteArrayList是无界List
  3. 在添加元素时,首先复制了一个快照,然后在快照上进行添加,而不是直接在原来数组上进行

add指定位置添加元素:

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 {
            // 如果要插入的位置在数组的中间,就需要拷贝 2 次
            // 第一次从 0 拷贝到 index。
            // 第二次从 index+1 拷贝到末尾。
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            // index 索引位置的值是空的,直接赋值即可。
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }

从上get到:

  1. 当插入的位置正好位于末尾时,只需要拷贝一次,当插入的位置处于中间时,此时我们会把原数组一分为二,进行两次拷贝操作。

删除操作和add方法类似,这里看下批量删除操作:

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) {
                // newlen 表示新数组的索引位置
                int newlen = 0;
                Object[] temp = new Object[len];
                // 循环,把不包含在 c 里面的元素,放到新数组中
                for (int i = 0; i < len; ++i) {
                    Object element = elements[i];
                    // 不包含在 c 中的元素,从 0 开始放到新数组中
                    if (!c.contains(element))
                        temp[newlen++] = element;
                }
                // 拷贝新数组,变相的删除了不包含在 c 中的元素
                if (newlen != len) {
                    setArray(Arrays.copyOf(temp, newlen));
                    return true;
                }
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

从上get到:
批量删除操作并不会直接对数组中的元素进行挨个删除,而是先将数组中的值进行循环判断,把我们不需要删除的数组放到临时数组中,最后临时数组中的数据就是我们不需要删除的数据。这样提升了性能

迭代器的弱一致性

弱一致性是指返回迭代器后,其它线程对list的增删改对迭代器是不可见的。

public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
        //array的快照
        private final Object[] snapshot;
        //数组下标
        private int cursor;
        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }
        public boolean hasNext() {
            return cursor < snapshot.length;
        }

        public boolean hasPrevious() {
            return cursor > 0;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }
}

如果没有其它线程对list进行增删改,那么snapshot本身就是list的array,因为他们是引用关系。但是如果在遍历期间其它线程对该list进行增删改,那么snapshot就是快照了,因为在增删改之后list里面的数组就被新数组替换了,它们操作的是两个不同的数组,这就是弱一致性。

总结

  1. CopyOnWriteArrayList仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致
  2. CopyOnWriteArrayList迭代器是只读的,不支持增删改。因为迭代器遍历的仅仅是一个快照,而对快照行增删改是没有意义的

你可能感兴趣的:(java,java)