Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。
(一)什么是CopyOnWrite容器?
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
(二)CopyOnWriteArrayList如何做到线程安全的?
CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。
当有新元素加入的时候,如下图,创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的。
当元素在新数组添加成功后,将array这个引用指向新数组。
(三)CopyOnWriteArrayList源码分析
1.继承与接口的关系关系,由此我们可以看出CopyOnWriteArrayList其实和一般的List集合实现类一样都实现了基本的接口。
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, Serializable
2.属性字段
private transient volatile Object[] elements;
volatile原理:
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
3.构造函数
public CopyOnWriteArrayList() {
elements = EmptyArray.OBJECT; //创建一个Object类型的空表
}
//将集合c复制到CopyOnWriteArrayList中
@SuppressWarnings("unchecked")
public CopyOnWriteArrayList(Collection<? extends E> collection) {
this((E[]) collection.toArray());
}
//将传入的数组复制到CopyOnWriteArrayList中
public CopyOnWriteArrayList(E[] array) {
this.elements = Arrays.copyOf(array, array.length, Object[].class);
}
4.增删改查操作
(1)添加操作
public synchronized boolean add(E e) {
Object[] newElements = new Object[elements.length + 1]; //这里是重新构造了一个新的数组
System.arraycopy(elements, 0, newElements, 0, elements.length);
newElements[elements.length] = e;
elements = newElements; //然后再把新的数组复制给原来的数组
return true;
}
CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。 这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了。下面的几个add方法也是同理。
public synchronized void add(int index, E e) {
Object[] newElements = new Object[elements.length + 1];
System.arraycopy(elements, 0, newElements, 0, index);
newElements[index] = e;
System.arraycopy(elements, index, newElements, index + 1, elements.length - index);
elements = newElements;
}
public synchronized boolean addAll(Collection<? extends E> collection) {
return addAll(elements.length, collection);
}
public synchronized boolean addAll(int index, Collection<? extends E> collection) {
Object[] toAdd = collection.toArray();
Object[] newElements = new Object[elements.length + toAdd.length];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(toAdd, 0, newElements, index, toAdd.length);
System.arraycopy(elements, index,
newElements, index + toAdd.length, elements.length - index);
elements = newElements;
return toAdd.length > 0;
}
由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:
1、如果写操作未完成,那么直接读取原数组的数据;
2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
可见,CopyOnWriteArrayList的读操作是可以不用加锁的。
(2)删除操作,也是开辟另一个数组来操作
public synchronized E remove(int index) {
@SuppressWarnings("unchecked")
E removed = (E) elements[index];
removeRange(index, index + 1);
return removed;
}
private void removeRange(int from, int to) {
Object[] newElements = new Object[elements.length - (to - from)];
System.arraycopy(elements, 0, newElements, 0, from);
System.arraycopy(elements, to, newElements, from, elements.length - to);
elements = newElements;
}
public synchronized boolean remove(Object o) {
int index = indexOf(o);
if (index == -1) {
return false;
}
remove(index);
return true;
}
public synchronized boolean removeAll(Collection<?> collection) {
return removeOrRetain(collection, false, 0, elements.length) != 0;
}
public synchronized boolean retainAll(Collection<?> collection) {
return removeOrRetain(collection, true, 0, elements.length) != 0;
}
(3)修改操作
public synchronized E set(int index, E e) {
Object[] newElements = elements.clone();
@SuppressWarnings("unchecked")
E result = (E) newElements[index];
newElements[index] = e;
elements = newElements;
return result;
}
(4)读取操作
@SuppressWarnings("unchecked")
public E get(int index) {
return (E) elements[index];
}
读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
(5)其他的集合方法基本和ArrayList一样了,就不在继续分析了。
通过上面的分析,CopyOnWriteArrayList 有几个缺点:
1、由于写操作的时候,存在内存占用的问题,因为每次对容器结构进行修改的时候都要对容器进行复制,这么一来我们就有了旧有对象和新入的对象,会占用两份内存。如果对象占用的内存较大,就会引发频繁的垃圾回收行为,降低性能;
2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用。因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
CopyOnWriteArrayList透露的思想,如上面的分析CopyOnWriteArrayList表达的一些思想:
1、读写分离,读和写分开
2、最终一致性 (CopyOnWrite只能保证数据最终的一致性,不能保证数据的实时一致性。)
3、使用另外开辟空间的思路,来解决并发冲突.
CopyOnWriteArrayList对我们还说还是了解有这么一个东西存在而已,一般Android都不会使用。
网友评论