CopyOnWriteArrayList学习笔记

Posted 花花young

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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迭代器是只读的,不支持增删改。因为迭代器遍历的仅仅是一个快照,而对快照行增删改是没有意义的

以上是关于CopyOnWriteArrayList学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

CopyOnWriteArrayList 学习笔记

CopyOnWriteArrayList学习

Java源码学习(JDK 11)——java.util.concurrent.CopyOnWriteArrayList

Java源码学习(JDK 11)——java.util.concurrent.CopyOnWriteArrayList

JDK1.8 CopyOnWriteArrayList源码学习

Java并发-CopyOnWriteArrayList