说明
CopyOnWrite容器我们可以理解为写的时候复制的容器,最简单的理解就是当我们往里面添加元素的时候,不直接往当前的容易添加,而是先将当前容易拷贝一份,复制成一个新的容器,然后在新的容器里添加元素,添加完元素以后,再将原容器的引用指向新的容器。这样子我们就非常轻松的实现了读写分离的操作。从单词的后半部分来看其内部存储跟ArrayList都是使用了数组进行数据存储的,而且添加、修改、删除、查询数据的方法名字都是一样的。
类结构图
源码分析
- 主要属性
//这个变量主要是用于同步代码加锁使用 synchronized (lock) 使用
final transient Object lock = new Object();
//该变量主要用于指向 实际存储数据元素的 数组
private transient volatile Object[] array;
- 构造方法
//默认构造函数,同时创建一个空数组,同时指向属性 array 变量
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
//首先判断 c为 CopyOnWriteArrayList 类型,直接通过 getArray获取数组
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
//拷贝数组元素
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
public CopyOnWriteArrayList(E[] toCopyIn) {
//拷贝数组元素
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
- 添加元素
最后位置添加元素
public boolean add(E e) {
//首先对代码块进行加锁,防止其他的线程修改 array数组
synchronized (lock) {
//首先获取旧数组 数组
Object[] elements = getArray();
int len = elements.length;
//创建一个新数组长度为 旧数组长度 + 1,同时将旧数据拷贝到新数组中。
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将要添加的数据赋值到新数组的末尾
newElements[len] = e;
//同时修改 array指向为新数组
setArray(newElements);
return true;
}
}
指定位置添加元素
public void add(int index, E element) {
//首先对代码进行加锁
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
//判断位置是否在数组中是否越界
if (index > len || index < 0)
throw new IndexOutOfBoundsException(outOfBounds(index, len));
Object[] newElements;
int numMoved = len - index;
//如果插入的位置是最后一个位置,则直接拷贝旧数组所有数据拷贝新数组中
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
//如果插入的位置不是最后一个位置,则需要分两次进行数组拷贝
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1, numMoved);
}
newElements[index] = element;
setArray(newElements);
}
}
- 删除元素
删除指定位置元素
public E remove(int index) {
//首先加锁
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
//获取旧数组中指定位置的数据
E oldValue = get(elements, index);
//计算后面需要移动元素的个数
int numMoved = len - index - 1;
//如果删除的元素的位置是最后一个位置的话,则将 len以前的数据拷贝到新数组中
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
//如果删除的不是最后一个元素,需要分两次进行数据拷贝到新数组中
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index, numMoved);
//再将新数组赋值给 array 成员变量
setArray(newElements);
}
//返回删除的数据
return oldValue;
}
}
删除具体元素
public boolean remove(Object o) {
Object[] snapshot = getArray();
//首先遍历数组,找到该元素所在的位置。index 小于0表示没有找到该元素,直接返回false
int index = indexOf(o, snapshot, 0, snapshot.length);
return (index < 0) ? false : remove(o, snapshot, index);
}
- 修改元素
指定位置修改元素
public E set(int index, E element) {
synchronized (lock) {
Object[] elements = getArray();
//首先获取旧数组中该位置的数据
E oldValue = get(elements, index);
//修改的新数据与之前的旧数据一致不一样
if (oldValue != element) {
int len = elements.length;
//拷贝一份新的数组数据
Object[] newElements = Arrays.copyOf(elements, len);
//直接 index 位置数据
newElements[index] = element;
setArray(newElements);
} else {
setArray(elements);
}
//返回修改前的旧数据
return oldValue;
}
}
- 查询元素
获取指定位置元素,支持随机访问。时间复杂度为 O(1)
public E get(int index) {
//获取元素不加锁,直接返回index位置的元素。
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
疑惑
根据上面的代码我们知道改变数组中的数据时都会进行加锁操作的,比如说添加、删除、修改都会对代码进行加锁,唯独获取数据的时候没有加锁,这样子就使得读写分离了。一般来讲我们在使用的时候,会用一个线程向容器中添加元素,一个线程来读取元素;那么就会出现一个问题:因为在写入数据的同时,需要拷贝一个新的数组,那么在读取的时候可能读取到是旧数据;与此相反的是如果写入数据的速度要先于读取的速度的,这个时候读取到的数据就是最新的数据,那么这里就会有两种情况了。综合起来我们可以分为以下几点:
- 如果写操作未完成,那么直接读取原数组的数据;
- 如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
- 如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
总结
- 由于写操作的时候需要拷贝数组,会消耗内存,如果原数组内容比较多的情况下,可能导致full GC
- 不能用于时时读的场景,像拷贝数组,新增元素都需要时间;虽然 能做到最终一致性,但是没法满足实时性要求
- 适合读多写少的场景,不过因为没法保证到底要放置多少数据,万一数据有点多的话,每次的操作(add/set/remove)都要重新复制数组,代价有点高。
- 虽然有这么多的缺点,但是它提供了一种读写分离、最终一致性的思想。