概述
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、使用另外开辟空间的思路,来解决并发冲突