并发容器之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分析的主要内容,如果未能解决你的问题,请参考以下文章