前景提要
前几天发了篇关于循环遍历列表的博文,下面继续补充为什么CopyOnWriteList可以进行遍历删除不报错,以及为什么不建议CopyOnWriteList进行频繁删除或新增。
源码分析
我们已经知道了list不能在循环中删除新增的本质原因是因为modCount和exceptModCount的值不一致导致的。
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String next = iterator.next();
if(next.contains("2")){
list.remove(next);
}
}
下面分析CopyOnWriteList为什么可以,直接上核心代码remove()方法
public boolean remove(Object o) {
//拿到现在的数组
Object[] snapshot = getArray();
//这个方法返回元素所在的下标,下面会简单介绍
int index = indexOf(o, snapshot, 0, snapshot.length);
//如果返回小于0,则返回false,否则执行remove方法
return (index < 0) ? false : remove(o, snapshot, index);
}
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
indexOf():
接收一个要搜索的元素 o,当前的数组 elements,搜索的起始索引 index,以及搜索范围的结束索引 fence。它遍历从 index 到 fence(不包括 fence)的数组元素,如果找到与 e 相等的元素,则返回该元素的索引;如果遍历完整个范围都没有找到,则返回 -1 表示未找到
我们可以看到关键代码是remove方法:
private boolean remove(Object o, Object[] snapshot, int index) {
//通过 ReentrantLock 加锁,确保在修改数组的过程中不会被其他线程打断
final ReentrantLock lock = this.lock;
lock.lock();
try {
//getArray方法,拿到现在的数组
Object[] current = getArray();
int len = current.length;
//如果传入的快照数组 snapshot 不等于当前的数组 current,说明在获取快照之后有其他线程修改了数组。因此,需要重新查找要删除元素的索引
if (snapshot != current) findIndex: {
//使用 findIndex 标签的循环来重新查找索引。这里只遍历到 index 和 len 中的较小值,因为我们已经知道在 snapshot 中索引 index 处的元素是需要删除的
int prefix = Math.min(index, len);
for (int i = 0; i < prefix; i++) {
//如果在遍历过程中发现 current 和 snapshot 在某个位置上的元素不同,并且该位置的元素等于要删除的对象 o,则更新 index 并跳出循环。
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
//如果循环结束后 index 大于等于 len,说明 o 不在当前数组中,返回 false
if (index >= len)
return false;
//如果 current[index] 仍然等于 o,则不需要进一步查找,直接跳出循环
if (current[index] == o)
break findIndex;
//如果以上条件都不满足,调用 indexOf 方法重新在 current 数组中查找 o 的索引。
index = indexOf(o, current, index, len);
//如果 indexOf 返回的索引小于 0,说明 o 不在当前数组中,返回 false
if (index < 0)
return false;
}
//如果找到了要删除的元素的索引,创建一个新的数组 newElements,其长度比当前数组少一个元素。然后使用 System.arraycopy 方法将当前数组中的元素复制到新数组中,同时跳过要删除的元素。
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
//调用 setArray 方法将 newElements 设置为当前数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
我们能够发现,这块代码并没有对modCount的改动,核心就是通过复制了一份新的数组,再进行赋值操作(此处跳过需要删除的元素),因此迭代器部分初始exceptModCount也是被赋值为modCount的0,迭代器对复制出来的删除元素并不感知,因此不会报错。
我们其实分析add()方法也能发现,他底层进行了复制数组操作,再赋值
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();
}
}
因此我们说并不建议在高写删的场景下进行使用copyOnWriteList,因为底层会进行大量的复制数组操作,这对于内存和性能都是很大的消耗。
对于copyOnwriteList的底层暂时研究到这里,粗略研究,欢迎讨论。