前言
ArrayList 是一个非线程安全集合,需要使用方自行处理线程安全问题,或者使用 Collections.synchronizedList 包装。从 JDK 1.5 开始 JUC 中提供了使用写时复制机制实现的并发容器 CopyOnWriteArrayList。
概述
CopyOnWriteArrayList 遵从 CopyOnWrite,顾名思义就是写时复制。简单来说就是当我们操作容器时并不直接操作当前容器,而是先根据当前容器复制出一个新的容器,然后在这个新的容器上操作,完成操作后再将原容器引用指向新容器。
属性
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/**
* 保护所有更改操作的锁
*/
final transient ReentrantLock lock = new ReentrantLock();
/**
* 内部数组。
* 所有的读操作都是基于当前 array 进行的;所有的写操作都会复制一个新的 array,然后在新复制的数组上执行操作。
*/
private transient volatile Object[] array;
}
CopyOnWriteArrayList 核心属性就是以上两个。lock 用于保护所有更改操作,控制并发写操作。array 作为内部数组用于保存数据,读操作都是操作当前 array,写操作都是根据当前 array 复制一个新的 array,然后在这个新的数组中进行操作。
新增
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 1 获得独占锁
lock.lock();
try {
// 2 获得 array 数组
Object[] elements = getArray();
// 3 获得 array 数组长度
int len = elements.length;
// 4 根据当前 array 复制一个新的数组,长度 +1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 5 将元素加入到新数组
newElements[len] = e;
// 6 替换旧数组
setArray(newElements);
return true;
} finally {
// 7 释放独占锁
lock.unlock();
}
}
下面对上述代码流程进行说明:
使用 ReentrantLock 独占锁保证同时只有一个线程对集合进行写操作。
数据存储在 CopyOnWriteArrayList 的内部 array 数组中。
元素并不是直接存储到 array 数组中,而是复制出一个新的数组,并且复制出的数组的长度是旧数组长度+1,然后将元素加入到新数组中,最后再把旧的数组替换成新的数组。
查询
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
由于整个 get 方法是没有加锁的,而 CopyOnWriteArrayList 的写操作是通过复制出新的数组来完成操作的,这可能会导致读写的短暂不一致。
修改
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
// 1 获得独占锁
lock.lock();
try {
// 2 获得当前 数组 array
Object[] elements = getArray();
// 3 根据下标,获得旧的元素
E oldValue = get(elements, index);
// 4 如果旧的元素不等于新的元素,等同于 add 方法,不过这里没有增加数组容量大小
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
// 5 为了保证 volatile 语义,即使元素没有改变也要替换成新的数组
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
// 6 释放独占锁
lock.unlock();
}
}
和 add 方法类似,不同的是对要修改的元素进行判断。
删除
public E remove(int index) {
final ReentrantLock lock = this.lock;
// 1 获取独占锁
lock.lock();
try {
// 2 获取对应下标元素
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
// 3 确定调整位置,并根据该位置移动元素
int numMoved = len - index - 1;
// 3.1 要删除的元素在末尾
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
// 3.2 要删除的元素在数组中间
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 {
// 4 释放锁
lock.unlock();
}
}
删除方法和其它修改方法类似,先是获取独占锁来保证线程安全,接着判断要删除的元素是否是最后一个,如果是最后一个则复制出一个长度-1的新数组然后替换掉旧数组;如果要删除的元素不是最后一个,则分两次复制,随后替换旧数组。
迭代器
/**
* 迭代器
*
* @param <E>
*/
static final class COWIterator<E> implements ListIterator<E> {
/**
* 数据快照
*/
private final Object[] snapshot;
/**
* 遍历数组元素的游标
*/
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
/**
* 判断是否还有下一个元素
*
* @return
*/
public boolean hasNext() {
return cursor < snapshot.length;
}
/**
* 判断是否有上一个元素
*
* @return
*/
public boolean hasPrevious() {
return cursor > 0;
}
/**
* 获取下个元素
*
* @return
*/
@SuppressWarnings("unchecked")
public E next() {
if (!hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (!hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
// 不支持写操作
public void remove() {
throw new UnsupportedOperationException();
}
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
}
当获取迭代器时,内部会调用 COWIterator 的构造方法,该构造方法有两个参数,一个是 array 数组,另一个是下标 0 。构造方法中会把 array 数组赋值给 snapshot 变量,当其他线程进行了增删改的操作,旧的数组会被新的数组给替换掉,但是 snapshot 还是原来旧的数组的引用。当我们使用迭代器遍历 CopyOnWriteArrayList 的时候,不能保证拿到的数据是最新的,这是弱一致性问题。
设计特点
CopyOnWriteArrayList 仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致。虽然采用了读写分离思想,但写操作时内存里会同时存在两个对象的内存,若这些对象占用内存较大,可能会带来系列问题,如造成频繁 GC。
小结
CopyOnWriteArrayList 是 Java 并发包中相对比较简单的一个实现,它的核心思想是写时复制机制,支持并发读写,但带来的后果是内存 double 和数据一致性问题。