CopyOnWriteArrayList 源码阅读与分析
CopyOnWriteArrayList是并发包下的一个线程安全、可以实现高并发的ArrayList
首先来看看它的构造方法:
final void setArray(Object[] a) {
array = a;
}
/**
* Creates an empty list.
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
可以看到,它和ArrayList有一定的不同,它是创建一个大小为 0 的数组。ArrayList是一个空数组。
下面来看看它的add(E) 方法,进一步了解它的实现原理
顾名思义,它的名字叫CopyOnWriteArrayList(),也就是在进行写操作时,如add,remove时,会进行复制操作,即创建一个新的比当前数长度大1的新的数组,再把旧的值复制过去。代码如下所示:
public boolean add(E e) {
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();
}
}
add 方法并没有使用 synchronized 关键字来实现互斥,而是通过使用ReentrantLock 来进行加锁操作。可以看到,它的实现比较简单,首先加锁,然后创建一个比目前数组长度大1的新数组,再把旧数组中的值复制到新的数组中去。然后再调用setArray方法改变引用,完成add的操作,结束以后,再释放锁。
之所以再每次的修改数组操作(add/remove等)之后,会新建一个数组,修改完毕之后,再将原来的引用指向新的数组。是为了保证数组被一个线程遍历时,没有其他线程对数组进行修改。所以也就不会抛出ArrayList并发情况下出现的ConcurrentModificationException错误。
下面再来看看get(int) 方法
先来看看代码的实现:
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
可以看到,非常简单,直接获取当前数组对应的位置的元素,但是由于方法没有进行任何的加锁操作,所以可能会出现读到脏数据的现象,这样可以有比较高的性能,所以在修改少,读取多的场景下。它开始很好的选择。
再来看看remove(E) 方法
和 add 方法一样,此方法也通过ReetrantLock 来保证其线程安全。
首先判断要删除的元素是不是最后一个元素:
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));
如果不是最后一个元素,就进行两次复制,先把原数组从0到index-1的数据复制到新的数组,再把index+1到最后一个元素复制到新的数组中,最后再改变引用。完成这些以后,就完成了删除的操作。
可以看到CopyOnWriteArrayList没有像ArrayList一样的动态扩容机制,而是再每次对数组修改时都会新建一个新的数组,为了避免ConcurrentModificationException异常,但是这样也会对性能有一定的影响。而且它并没有对读操作进行任何的线程安全的操作,所以可能会出现读到脏数据的情况。所以当写操作比较多时,使用CopyOnWriteArrayList个人感觉不是好的选择。