CopyOnWriteArrayList使用场景和源码分析

 (1)ArrayList和CopyOnWriteArrayList的增删改查实现原理?
 (2)为什么说ArrayList查询快而增删慢?
 (3)弱一致性的迭代器原理是怎么样的?
 (4)CopyOnWriteArrayList为什么并发安全且性能比Vector好?
 (5)JDK中为什么没有CopyOnWriteLinkedList?

        对于CopyOnWriteArrayList面试的话,容易和ArrayList放在一起,让你比较他们之间的异同点,这一点需要有所准备。本文将重点讲解CopyOnWriteArrayList,对于这个JUC下面的包,其源码是很简单的。

       对于ArrayList在并发情况下使用是不安全的,看一下这个例子:

    @Test
    public void testList(){
        List<String> list = Lists.newArrayList("a", "b", "c", "d");
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        Iterator<String> iterator = list.iterator();
        for (int i = 0; i < 1; i++) {
            threadPool.execute(()->{
                for (int j = 0; j < 5; j++) {
                    list.add( j * 8 + "");
                }
            });
        }

        for (int i = 0; i < 10; i++) {
            threadPool.execute(()->{
                while (iterator.hasNext()){
                    System.out.println(iterator.next());
                }
            });
        }
    }

        以上是因为有一个线程在向list中添加数据,有五个线程从list中遍历读取数据,这个时候就会报错,其错误信息如下:

         

       对于这种可以使用线程安全的List,如Collections.synchronizedList,但是对于那种一年可能也不改一次,而经常需要去进行读取遍历的场景来说,简直太坑了,所以这个时候就出来了CopyOnWriteArrayList这个集合类。

        CopyOnWriteArrayList使用到了Copy-On-Write思想,写入时复制,这个技术,是一个写入时复制的容器,它是如何工作的呢?简单来说,就是平时查询的时候,都不需要加锁,随便访问,只有在写入/删除的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。这点要跟读写锁区分一下。

        在CopyOnWriteArrayList开头有这样一段注释:

A thread-safe variant of {@link java.util.ArrayList} in which all mutative operations ({@code add}, {@code set}, and so on) are implemented by making a fresh copy of the underlying array.

 <p>This is ordinarily too costly, but may be <em>more</em> efficient than alternatives when traversal operations vastly outnumber mutations, and is useful when you cannot or don't want to synchronize traversals, yet need to preclude interference among concurrent threads.  The "snapshot" style iterator method uses a reference to the state of the array at the point that the iterator was created. This array never changes during the lifetime of the iterator, so interference is impossible and the iterator is guaranteed not to throw {@code ConcurrentModificationException}.
 The iterator will not reflect additions, removals, or changes to the list since the iterator was created.  Element-changing operations on iterators themselves ({@code remove}, {@code set}, and {@code add}) are not supported. These methods throw {@code UnsupportedOperationException

         不想看英文的可以看以下使用翻译的中文:

       CopyOnWriteArrayList是ArrayList的一种线程安全的变体,在add、set、remove等会改变其内部值和长度的时候会通过创建一个新的数组来进行实现。
       这通常代价太高,但是当遍历操作的数量远远超过修改数量时,可能比替代操作的效率更高,并且在不希望在同步遍历时受到其他并发线程的干扰是非常有用的。“快照”样式的迭代器方法使用对迭代器创建时数组状态的引用。在迭代器的生存期内,这个数组永远不会改变,因此干扰是不可能的,并且迭代器保证不会抛出{@code ConcurrentModificationException}。
迭代器不会反映自迭代器创建以来对列表的添加、删除或更改。不支持对迭代器本身({@code remove}、{@code set}和{@code add})执行元素更改操作。这些方法抛出{@code unsupportedperationexception。

        对于CopyOnWriteArrayList来说,读的时候不加锁,只有在写的时候才加锁,适用于读操作远远大于写操作、而且不希望读的时候也去加锁,不希望在同步遍历时受到其他并发线程的干扰而错误错误的场景。在这种场景下使用CopyOnWriteArrayList非常适合。

        下面看一下CopyOnWriteArrayList的类继承关系:

                  

         实现了RandomAccess接口,说明支持顺序方法(RandomAccess接口没有任何方法,只是一个标记而已),使用了Cloneable说明可以进行拷贝,实现Serializable接口说明支持序列化,实现List接口说明可以CopyOnWriteArrayList会实现List的所有方法。

         

         首先看一下CopyOnWriteArrayList的属性,重要的就两个,使用ReentrantLock来对写操作进行加锁,使用volatile修饰的Object[]保存数据,下面先看一下读操作CopyOnWriteArrayList是怎么做的:

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }
    
    public boolean contains(Object o) {
        Object[] elements = getArray();
        return indexOf(o, elements, 0, elements.length) >= 0;
    }    
    
    public int size() {
        return getArray().length;
    }
    
    public int indexOf(Object o) {
        Object[] elements = getArray();
        return indexOf(o, elements, 0, elements.length);
    }
    
    private E get(Object[] a, int index) {
        return (E) a[index];
    }   
     
    private E get(Object[] a, int index) {
        return (E) a[index];
    }
    
    private static int indexOf(Object o, Object[] elements,
                               int index, int fence) {
        if (o == null) {
            for (int i = index; i < fence; i++)
                if (elements[i] == null)
                    return i;
        } else {
            for (int i = index; i < fence; i++)
                if (o.equals(elements[i]))
                    return i;
        }
        return -1;
    }

        可以看到读操作完全没有加锁,这样查询的性能会很高,最终都是查询Object[]数组里面保存的数据。

        下面来看一下add方法:

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        //1.先进行加锁
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //2.拷贝一个新的数组,此时数据的长度是被拷贝数组长度+1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //3.赋值
            newElements[len] = e;
            //4.将新的数组赋值给array
            setArray(newElements);
            return true;
        } finally {
            //5.释放锁
            lock.unlock();
        }
    }

    final Object[] getArray() {
        return array;
    }

    final void setArray(Object[] a) {
        array = a;
    }

         可以看到在add操作时,会加锁,然后进行操作,需要注意一点,Arrays.copyOf会拷贝一个新的数组,属于深拷贝,会在内存中创建一个数组来存放值,Arrays.copyOf底层是调用native方法进行操作,数组在内存中是一块连续的区域,这样做实现拷贝数组的速度会很快,这一点也就能解释了为什么JDK中没有CopyOnWriteLinkedList的实现,因为CopyOnWriteLinkedList是链表形式的,链表的拷贝好像没有native方法,所以在拷贝时,只能手动在java代码层面进行操作,这样的速度在高并发的场景下是不能忍受的,所以JDK中也就没有CopyOnWriteLinkedList的实现。

        下面来看一下remove方法:

    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        //1.加锁
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //2.所以该下标元素
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            //3.进行判断,其需要移除元素的下标在数组的哪个位置,然后进行相应的移除
            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 {
            //4.释放锁
            lock.unlock();
        }
    }

        其操作也是先加锁,然后进行操作,创建一个新数组,然后将新数组赋值给array。

        对于写操作的其他方法也是这样的,操作之前加锁,操作之后释放锁,对于CopyOnWriteLinkedList的源码实现是很简单的,主要是理解其需要在什么场景下面使用,这个才是关键,对于上面5个问题,我就不一一解释了,其实看到这里大家都有答案,对于第5条这个在add方法时,也说了。

总结:

        CopyOnWriteArrayList是使用Copy-On-Write思想来进行设计的,在读操作的时候不加锁,在写操作时会进行加锁,在进行写操作时,会创建一个数组的副本,所以的操作在这个副本中进行,在操作完成后,将之前的数组给替换成操作完成的副本。适用于读操作远远大于写操作、而且不希望读的时候也去加锁,不希望在同步遍历时受到其他并发线程的干扰而错误错误的场景。

      优点:

       对于一些读多写少的数据,这种做法的确很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。

       缺点:

       这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。如果对象比较大,频繁地进行替换会消耗内存,从而引发Java的GC问题,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章