CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
我们通过它的实现源码来解读
public boolean add(E e) { //1、先加锁 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; //2、拷贝数组 Object[] newElements = Arrays.copyOf(elements, len + 1); //3、将元素加入到新数组中 newElements[len] = e; //4、将array引用指向到新数组 setArray(newElements); return true; } finally { //5、解锁 lock.unlock(); } }可见写的时候首先要获取锁,保证只有一个线程在进行写操作,防止并发
原理就是拷贝一份新的内存区域,在新的区域对数组进行修改,然后让之前的数组指针指向新的数组内存区域
由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况: 1、如果写操作未完成,那么直接读取原数组的数据; 2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据; 3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
可见,CopyOnWriteArrayList的读操作是可以不用加锁的。
我们通过一个例子来说明一下:
public class Test { public static void main(String[] args) { List<Integer> list = Arrays.asList(new Integer[] { 1, 2, 3, 4 }); CopyOnWriteArrayList<Integer> copyList = new CopyOnWriteArrayList<Integer>(list); ExecutorService executorService = Executors.newFixedThreadPool(10); executorService.execute(new ReadList(copyList)); executorService.execute(new ReadList(copyList)); executorService.execute(new WriteList(copyList)); executorService.execute(new ReadList(copyList)); executorService.execute(new WriteList(copyList)); executorService.execute(new ReadList(copyList)); executorService.execute(new ReadList(copyList)); executorService.shutdown(); } } class ReadList implements Runnable { private List<Integer> list; public ReadList(List<Integer> list) { this.list = list; } /** * @see java.lang.Runnable#run() */ @Override public void run() { for (int i : list) { System.out.println("读取list, i = " + i); } } } class WriteList implements Runnable { private List<Integer> list; public WriteList(List<Integer> list) { this.list = list; } /** * @see java.lang.Runnable#run() */ @Override public void run() { for (int i = 0; i < 5; i++) { list.add(10); System.out.println("写list"); } } }通常情况下,当你对一个集合既有写操作也有读操作的时候,就会报:java.util.ConcurrentModificationException 的错误
可见通过CopyOnWriteArrayList是线程安全的处理方式
CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。
内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。
数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
CopyOnWriteArrayList表达的一些思想: 1、读写分离,读和写分开 2、最终一致性 3、使用另外开辟空间的思路,来解决并发冲突