一次偶然的机会接触到了CopyOnWriteArrayList这个容器,当时就对这个名字很长的容器就产生了兴趣,后来闲暇的时候就研究了一下这个容器,下面就简单介绍一下这个容器的使用方式及其实现原理。
CpoyOnWrite(COW),写时复制,是一种应用场景很多的技术,在fork子进程,Redis数据库的持久化等等都有COW的影子,CopyOnWriteArrayList是ArrayList的线程安全体,在使用ArrayList时,如果多线程操作,遍历的时候,如果被修改了会抛出java.util.ConcurrentModificationException错误,除此之外,如果有多个线程同时对List里面的元素进行更改操作,很可能会出现我们预期之外的结果。CopyOnWriteArrayList是ArrayList在读多写少场景下的线程安全的适用版本,下面从源码的方向出发,深入解读一下这个容器的原理。
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** 先加个锁,保证最多只能同时存在两个list*/
final transient ReentrantLock lock = new ReentrantLock();
/** volatile关键字保证对array的引用的可见性 */
private transient volatile Object[] array;
/**set与get方法*/
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
/**创建一个空的容器*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
........
}
下面看一下它的包含参数的构造方法:
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
//如果二者类型一致,直接将引用赋给elementts
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
//如果c的类型与CopyOnWriteArrayList不一致,执行拷贝操作,将c里面包含的元素拷贝到新建的
elements数组中
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
下面看一下add操作与remove操作:
//指定下标的add操作(如果没有指定下标默认添加到数组末尾)
public void add(int index, E element) {
//加锁,保证只能有一个线程执行成功这个方法
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//如果指定的下标越界,则抛出异常
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
//如果指定下标满足条件,则先把index之前与之后的元素拷贝进新数组,最后把index位置的
值置为我们指定值
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);
} finally {
lock.unlock();
}
}
//指定下标的remove方法,与add方法类似,都是将数组分成两个部分进行拷贝(remove必须指定下标)
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1;
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);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
最主要的部分就是上面几个方法,可以看出CopyOnWriteArrayList的源码部分还是比较简单易懂,这里只是COW在集合里面的一个小应用,显而易见,这种容器的适用场景就是读多写少,在这种场景中,可以最大限度保证执行效率。
下面就介绍一下高大上的COW技术常见的应用场景:
1.最常见的就是在文件系统中使用,为了保证操作文件时不会因为突然断电等原因丢失数据,在文件系统中就采用了COW技术,在这类场景中,当我们去修改文件的时候,实际上修改的只是文件的拷贝,这时发生的读取操作还是读取的原文件内容,万一修改过程出现错误,原文件还在,不会造成太大损失。当修改完成后,替换原文件。
2.linux中创建轻量级的子进程,我们都知道,进程是操作系统中比较昂贵的资源,它具有自己的数据和程序。传统方式下,fork()函数在创建子进程时直接把所有资源复制给子进程,这种实现方式简单,但是效率低下,而且复制的资源可能对子进程毫无用处。linux为了降低创建子进程的成本,改进fork()实现方式使用COW技术创建子进程。当父进程创建子进程时,内核只为子进程创建虚拟空间,父子两个进程使用的是相同的物理空间。只有父子进程发生更改时才会为子进程分配独立的物理空间,这样就可以大幅度节约资源。
3.Redis数据库中的使用,Redis在执行持久化操作的时候,有一种RDB的持久化方式,这种方式的其中一种持久化语句是执行BGSAVE操作,BGSAVE命令会派生一个子进程,由子进程来处理RDB文件的创建工作,由父进程处理服务器请求,如果服务器并未发生数据的修改状况,父进程就与子进程共享同一个物理空间,只有在发生数据修改时,才会拷贝一份数据给子进程。