CopyOnWriteArrayList源码解析
- 1.整体架构
- 2.源码解析
- 新增元素
- 尾部新增元素
- 添加至某索引处
- 批量添加
- 删除元素
- 索引位置删除
- 批量删除
- 其他方法
- indexOf方法
- 迭代
- 总结
ArrayList在作为共享变量存在时,是线程不安全的。JDK中保证对ArrayList操作是线程安全的方法有如下3种,
- 编程过程中对共享变量进行操作的代码自行加锁
- 使用Collections.synchronizedList方法
- 使用CopyOnWriteArrayList类
1.整体架构
整体架构上,CopyOnWriteArrayList的数据结构与ArrayList是相同的,底层是数组。只不过CopyOnWriteArrayList在对其数组数据进行操作的过程中会分为4步:
- 加锁
- 对原数组进行拷贝,得到新的数组,新数组除内存地址外其余都与原数组相同
- 新数组上进行操作,操作完成后原数组指向新数组的引用
- 解锁
除了加锁之外,CopyOnWriteArrayList的数组还被volatile关键字修饰,意思是一旦数组被修改,其余线程可以立刻感知到并更新引用。整体而言,CopyOnWriteArrayList就是利用锁+数组拷贝+volatile关键字保证了List的线程安全。
CopyOnWriteArrayList的构造方法如下,
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 内部的锁对象,保证一个时刻仅有一个线程持该锁
final transient ReentrantLock lock = new ReentrantLock();
// volatile关键字修饰的数组
private transient volatile Object[] array;
// 无参数初始化,初始化一个空数组
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
// 使用集合初始化
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
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);
}
// 使用数组初始化
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
......
}
CopyOnWriteArrayList的类注释中包含该类的一些信息,
- 所有的操作都是线程安全的,因为都是在新拷贝的数组上进行的
- 数组的拷贝有一定的成本,但是往往比其他的替代方法要高效
- CopyOnWriteArrayList在得到过程中如果原数组被修改,不会抛出ConcurrentModificationException异常
2.源码解析
新增元素
新增元素与ArrayList一样,支持数组尾部、某索引处、批量新增等。
尾部新增元素
尾部新增实现方式最简单,源码如下,
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 得到底层的原数组
Object[] elements = getArray();
int len = elements.length;
// 原数组元素拷贝到新数组里面,新数组的长度是 原数组长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 在新数组中进行赋值,新元素直接放在数组的尾部
newElements[len] = e;
// 替换掉原来的数组
setArray(newElements);
return true;
// finally 里面释放锁,保证即使 try 发生了异常,仍然能够释放锁
} finally {
lock.unlock();
}
}
源码中可知,整个add过程都是在持锁状态下进行的,保证一个时刻内只有一个线程持锁。
除了加锁之外还会从原数组中创建一个新数组,把原数组的值拷贝到新数组上。此处有一个需要理解的关键点。为什么在上锁的情况下,仍然需要拷贝数组而不是在原数组上进行操作?
- volatile关键字修饰的是数组,如果简单的在原数组上修改几个元素的值,是无法触发内存可见性的。只有通过修改数组地址的方式才能触发数组可见性,所以需要重新赋值。
- 在新的数组上进行拷贝,对原数组没有任何影响。只有新数组完全拷贝成功后,外部才能访问到,降低了赋值过程中原数组数值变动的影响。
添加至某索引处
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;
// numMoved相当于添加在末尾
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);
} finally {
lock.unlock();
}
}
基本原理与add方法相同,只不过在拷贝原数组过程中进行了两次复制。
批量添加
批量添加比使用for循环调用add方法的效率要高很多,
public boolean addAll(Collection<? extends E> c) {
Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ?
((CopyOnWriteArrayList<?>)c).getArray() : c.toArray();
if (cs.length == 0)
return false;
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 如果当前CopyOnWriteArrayList底层是空数组
if (len == 0 && cs.getClass() == Object[].class)
setArray(cs);
else {
// 一次性扩容操作
Object[] newElements = Arrays.copyOf(elements, len + cs.length);
System.arraycopy(cs, 0, newElements, len, cs.length);
setArray(newElements);
}
return true;
} finally {
lock.unlock();
}
}
高效的原因在于只进行了一次扩容操作。
删除元素
删除元素的过程与添加元素十分相似,分为3步,
- 加锁
- 判断索引位置,并对数组进行拷贝移除要删除的元素
- 解锁
索引位置删除
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 {
// 如果删除的数据在数组的中间,分三步走
// 1. 设置新数组的长度减一,因为是减少一个元素
// 2. 从 0 拷贝到数组新位置
// 3. 从新位置拷贝到数组尾部
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();
}
}
删除过程与添加过程都是在持锁状态下进行,同时底层数组会指向新的内存地址。
批量删除
批量删除源码如下,
public boolean removeAll(Collection<?> c) {
if (c == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 数组有值继续进行,若数组为空直接返回 false
if (len != 0) {
// newlen 表示新数组的索引位置,新数组中存在不包含在 c 中的元素
int newlen = 0;
// 注意新数组temp与原数组相同长度
Object[] temp = new Object[len];
// 循环,把不包含在 c 里的元素放到新数组中
for (int i = 0; i < len; ++i) {
Object element = elements[i];
// 不包含在 c 中的元素,从 0 开始放到新数组中
if (!c.contains(element))
temp[newlen++] = element;
}
// 拷贝新数组,变相的删除了不包含在 c 中的元素
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
lock.unlock();
}
}
上方代码中,新建的数组初始化长度与原数组相同,主要是考虑到集合C中的元素并非全部是数组中的元素。如果一次性初始化为的长度可能会出错。
批量删除操作比使用for循环调用remove方法的效率高的多,主要是避免了重复多次的数组拷贝。
其他方法
indexOf方法
该方法用于查找数组中指定元素下标的位置。如果存在则返回索引值;反之,返回-1。支持null值搜索,还支持正向和反向查找,以正向查找为例,
// o:需要搜索的元素
// elements:底层数组
// index:搜索的开始位置
// fence:搜索的结束位置
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
// 支持对 null 的搜索
if (o == null) {
for (int i = index; i < fence; i++)
// 找到第一个 null 值,返回下标索引的位置
if (elements[i] == null)
return i;
} else {
// 通过 equals 方法来判断元素是否相等
// 如果相等,返回元素的下标位置
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
indexOf方法通过equals方法判断元素是否相等,所以如果传入的是自定义类对象,需要实现equals方法。
迭代
CopyOnWriteArrayList在得到过程中如果原数组被修改,不会抛出ConcurrentModificationException异常的原因在于其迭代器的实现方式。
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
......
}
COWIterator类时CopyOnWriteArrayList类中的内部类,在调用Iterator方法时,会创建COWIterator类对象,snapshot引用的是当前底层数组的地址,这一点与ArrayList区分。
下面的例子进行说明,在迭代过程中修改原数组是不会影响迭代器的。因为迭代器引用的是原数组,而CopyOnWriteArrayList修改是在新数组上进行的。
对CopyOnWriteArrayList对象进行add操作,
操作后,list对象底层的数组引用是新的内存地址。此时对Iterator对象进行迭代,
迭代器对象引用的还是list对象修改之前的数组的内存地址,所以即使list发生变动,不会影响迭代器。
总结
在线程不安全的情况下,建议使用CopyOnWriteArrayList替代ArrayList对象。