并发容器之CopyOnWriteArrayList分析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发容器之CopyOnWriteArrayList分析相关的知识,希望对你有一定的参考价值。

       今天介绍的主角是CopyOnWriteArrayList类,是jdk1.5才加入的一个并发集合类,它是ArrayList的Thread-safe的变体,属于COW的一种,COW系列的还有CopyOnWriteArraySet集合。COW是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。

先给出结论:

       CopyOnWriteArrayList适用于读操作比写操作多很多的并发场景。如果读操作和写操作的频次相差不大时,建议使用Collections.synchornizedList。

 

先看CopyOnWriteArrayList的类声明

public class CopyOnWriteArrayList<E>
extends Object
implements List<E>, RandomAccess, Cloneable, Serializable

  

1. CopyOnWriteArrayList的由来

     CopyOnWriteArrayList是在Jdk1.5的concurrent包中引入的,concurrent包的类都是为了高效并发才引入的。Jdk1.5以前,针对并发场景,能使用List的方式只能通过Collections.synchornizedList方式产生或是自己使用synchronized关键字(Vector类,因效率过低已被废弃)来实现。我们知道容器在多线程读与读之间是并不存在资源竞争的。所以直接使用synchornized实现,在某些场景下,并不高效。由此就产生了CopyOnWriteArrayList。

 

2. CopyOnWriteArrayList与Collections.synchornizedList的性能比较

上代码

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.*;

/**
 * Created by Administrator on 2017/9/4.
 */
public class ListDemo {

    private static final int SIZE = 10000;

    public static long testAddList(List<Integer> list){
        long startTime = System.currentTimeMillis();
        for(int i = 0; i <SIZE; i++){
            list.add(i);
        }
        long time= System.currentTimeMillis() - startTime;
        return time;
    }

    public static long testGetList(List<Integer> list){
        long start = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            list.get(i);
        }
        long time= System.currentTimeMillis() - start;
        return time;
    }


    public static void main(String [] args){
        ArrayList<Integer> list = new ArrayList<>();
        List<Integer> list2 = Collections.synchronizedList(list);
        CopyOnWriteArrayList<Integer> list3 = new CopyOnWriteArrayList<>();

        //多线程测试性能;
        long addSynchronizedListTime = 0L, addCopyOnWriteArrayListTime = 0L;
        long getSynchronizedListTime = 0L, getCopyOnWriteArrayListTime = 0L;
        ExecutorService service = Executors.newCachedThreadPool();

        //测试synchornizedList的写和读操作的性能;
        for(int i = 0 ; i <5; i++) {
            try {
                addSynchronizedListTime += service.submit(new AddDataRunnable(list2)).get();
                getSynchronizedListTime += service.submit(new GetDataRunnable(list2)).get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }

        //测试CopyOnWriteArrayList的写和读操作性能;
        for(int i = 0 ; i <5; i++) {
            try {
                addCopyOnWriteArrayListTime += service.submit(new AddDataRunnable(list3)).get();
                getCopyOnWriteArrayListTime += service.submit(new GetDataRunnable(list3)).get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }

        System.out.println("addSynchornizedTime:"+addSynchronizedListTime);
        System.out.println("getSynchornizedTime:"+getSynchronizedListTime);
        System.out.println("addCopyOnWriteArrayListTime:"+addCopyOnWriteArrayListTime);
        System.out.println("getCopyOnWriteArrayListTime:"+getCopyOnWriteArrayListTime);

    }

    static class AddDataRunnable implements Callable<Long>{

        private List<Integer> mList;
        public AddDataRunnable(List<Integer> l){
            this.mList = l;
        }

        @Override
        public Long call() throws Exception {
            long time = testAddList(mList);
            return time;
        }
    }

    static class GetDataRunnable implements Callable<Long>{

        private List<Integer> mList;
        public GetDataRunnable(List<Integer> l){
            this.mList = l;
        }

        @Override
        public Long call() throws Exception {
            long time = testGetList(mList);
            return time;
        }
    }

}

运行结果如下:

addSynchornizedTime:3
getSynchornizedTime:3
addCopyOnWriteArrayListTime:1324
getCopyOnWriteArrayListTime:0

Process finished with exit code 0

可以从运行结果中得出结论:

Collections.synchronizedList的整体的读与写性能都比较稳定。而CopyOnWriteArrayList在写方面,表现的非常差,在读操作上,却非常优秀。

所以CopyOnWriteArrayList适合使用在读操作比较多的并发场景。

 

3. CopyOnWriteArrayList的代码分析

       我们接下来分析下为何CopyOnWriteArrayList有此特性,前面已经提到,针对读操作,是不做处理,和普通的ArrayList性能一样。而在写操作(修改时),会先拷贝一份,实现新旧版本的分离,然后在拷贝的版本上进行修改操作,修改完后,将其更新至就版本中。

我们以add方法为例:

    

/** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array; 

   /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }


/**
     * 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;
        lock.lock(); //加锁,防止多个写操作造成数据不一致问题;
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1); //构造一个新数组,并将旧的数据拷贝至新的数组中;
            newElements[len] = e;  //对新数组执行add操作;
            setArray(newElements);//将新数组更新至arrays
            return true;
        } finally {
            lock.unlock(); //释放锁;
        }
    }

  再看一下get方法

/**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }


/**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }

  get方法中没有加锁,就是普普通通的ArrayList的get操作;

       

       这里我们就可以知道CopyOnWriteArrayList面对写操作为什么性能低下了?因为首先需要去lock,有可能需要等待时间去获取锁,还有就是每一步的写操作,都会发生Arrays.copyOf的拷贝操作。

 

4. ConcurrentModificationException异常

    普通的ArrayList在遍历成员时,如果修改集合,则会报出ConcurrentModificationException异常。而CopyOnWriteArrayList的实现,在遍历时,修改并不会报出该异常。

    

import java.util.ArrayList;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Created by Administrator on 2017/9/4.
 */
public class ExceptionDemo {

    public static void main(String [] args){

//        ArrayList<Integer> list = new ArrayList<>();
//        list.add(1);
//        list.add(2);
//        list.add(3);
//
//        for(int a: list){
//            list.add(5);
//        }

        /*
        Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
        at java.util.ArrayList$Itr.next(ArrayList.java:851)
        at ExceptionDemo.main(ExceptionDemo.java:18)

        Process finished with exit code 1
         */

        CopyOnWriteArrayList<Integer> l = new CopyOnWriteArrayList<>();
        l.add(1);
        l.add(2);
        l.add(3);

        for(int a: l){
            l.add(5);
        }

    }
}

5. CopyOnWriteArrayList的缺点

    COW思想,是一种实现读写分离的思想,优化了读操作的性能。因为其实现所以存在以下的缺点:

  内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,会造成GC的回收,引发性能问题。针对内存紧张的场景,建议使用其他的并发容器代替。  

  数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

 

参考链接:

http://www.cnblogs.com/dolphin0520/p/3938914.html

http://blog.csdn.net/zljjava/article/details/48139465

 

以上是关于并发容器之CopyOnWriteArrayList分析的主要内容,如果未能解决你的问题,请参考以下文章

16.并发容器之CopyOnWriteArrayList

并发容器之CopyOnWriteArrayList

并发容器之CopyOnWriteArrayList

并发容器之CopyOnWriteArrayList分析

Java并发--并发容器之CopyOnWriteArrayList

Java并发编程:并发容器之CopyOnWriteArrayList