CopyOnWriteArrayList是J.U.C包下的一个并发容器,它是线程安全且读操作无锁的ArrayList,写操作(增删改)则通过将底层数组拷贝一份,更改操作全部在新数组上进行,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",Java并发包中类似的容器还有CopyOnWriteArraySet。本文会对CopyOnWriteArrayList的实现原理及源码进行分析。
一、实现原理
我们都知道,集合框架中的ArrayList是非线程安全的,Vector虽是线程安全的,但由于简单粗暴的锁同步机制,性能较差。而CopyOnWriteArrayList则提供了另一种不同的并发处理策略(当然是针对特定的并发场景)。
很多时候,我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。
1、关键属性和构造方法
在jdk1.8中CopyOnWriteArraylist的底层数据结构还是数组,它使用ReentrarntLock来保证线程安全
//在jdk1.8中使用ReentrantLock来保证线程安全,在jdk11使用的是synchronized
final transient ReentrantLock lock = new ReentrantLock();
//底层数据结构是Object数组
private transient volatile Object[] array;
//默认无参构造方法,初始化了一个长度为0的Object数组
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
//可以传一个数组,注意到他没有直接使用我们传入的数组,而是copy了一份,这是为啥呢?
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
2、添加元素—add()
add()方法的逻辑非常简单,在添加元素之前先获取当前对象的锁,之后在原数组的基础上复制一个新数组并且长度+1,之后把新元素放入新增加的数组索引位置之后在把老数组的引用改为这个新数组的引用,解锁成功后就完成了添加元素的逻辑。
public boolean add(E e) {
//ReentrantLock加锁,保证线程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//拷贝原容器,长度为原容器长度加一
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新副本上执行添加操作
newElements[len] = e;
//将原容器引用指向新副本
setArray(newElements);
return true;
} finally {
//解锁
lock.unlock();
}
}
3、删除操作
看了添加操作,再来看看删除操作,和add方法逻辑大致相同,都是需要先获取锁,如果移除的数据在数组末尾免责直接复制原数组的[0-array.length-1]
即可;如果要删除的元素不在数组末尾,那就会先new一个是原数组长度-1的新数组,然后使用System.arrayCopy
把原数组中除要删除元素之外的其他元元素复制过去。
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)
//如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
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();
}
}
4、获取元素—get()
CopyOnWriteArrayList允许并发的读,就是因为他没有加任何锁,任何线程只要来了就会可以获取数据。为啥敢这么写呢?这是因为CopyOnWriteArrayList中的Object数组是被volatile保护着,volatile可以保证共享数据在多个线程之间的可见性,即任何线程去获取值都是最新的值。
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
二、总结—CopyOnWriteArrayList优缺点分析
优点:
读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。Java的list在遍历时,若中途有别的线程对list容器进行修改,则会抛出ConcurrentModificationException异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了,这就是一种 安全失败机制 (并发容器几乎都是这种机制)。
缺点:
缺点也很明显,一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC;二是无法保证实时性,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读不会阻塞但读取到的却是老容器的数据。