介绍
CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",Java并发包中类似的容器还有CopyOnWriteSet。
源码分析:
add(E e)方法
public boolean add(E e) {
//加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取数组,我们知道ArrayList底层是基于数组实现的
Object[] elements = getArray();
//获取数组的长度
int len = elements.length;
//拷贝一个新数组,在原数组的长度上加1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将要添加的值放到新数组的最后一位
newElements[len] = e;
//将属性指向新数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
//getArray方法
final Object[] getArray() {
return array;
}
//setArray方法
final void setArray(Object[] a) {
array = a;
}
这是add的逻辑,主要就是将底层的数组拷贝一份,然后在新数组上进行添加,添加完成后将底层的数组指向新数组,返回。这个操作是要加锁的
remove(int index)
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);
//计算index下标之后的元素的个数
int numMoved = len - index - 1;
//这个值为0的时候说明删除的是最后一个元素,
//直接拷贝数组将前n-1个值复制进去返回即可
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
//删除的不是最后一个元素,创建新数组
Object[] newElements = new Object[len - 1];
//将原数组从0下标开始,index个元素复制到新数组中
System.arraycopy(elements, 0, newElements, 0, index);
//将原数组中从index+1下标开始,numMoved个元素复制到新数组中
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
//将引用指向新数组
setArray(newElements);
}
//返回被删除的值
return oldValue;
} finally {
//解锁
lock.unlock();
}
}
//get方法
private E get(Object[] a, int index) {
return (E) a[index];
}
这是根据数组下标删除元素,int numMoved = len - index - 1;这句可能有的人不懂,解释一下,
如果index = len - 1 (数组中最后一个元素),那么numMoved = 0;
如果index = 0 (数组中第一个元素),那么numMoved = len - 1;
所以显而易见,numMoved 表示的就是要删除的元素的下标index后要拷贝的元素个数。
remove(Object o)方法
public boolean remove(Object o) {
//获取当前数组
Object[] snapshot = getArray();
//获取要删除的值o的下标
int index = indexOf(o, snapshot, 0, snapshot.length);
//如果找不到元素下标,返回false
//如果找到元素下标,执行删除操作
return (index < 0) ? false : remove(o, snapshot, index);
}
//indexOf方法
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
//如果o为null,获取数组中第一个为null的元素的下标
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
//如果不为null,返回数组中第一个zhi为o的元素的下标
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
//找不到元素,返回-1
return -1;
}
//remove方法
private boolean remove(Object o, Object[] snapshot, int index) {
//加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取当前的数组
Object[] current = getArray();
//获取当前数组的长度
int len = current.length;
//如果取出的快照数组和当前数组不是同一个数组,
//说明,在删除期间,有别的线程动过数组
//findIndex -> 给代码块做个标记,可以退出代码块
if (snapshot != current) findIndex: {
int prefix = Math.min(index, len);
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i] && eq(o, current[i])) {//(1)
index = i;
break findIndex;
}
}
if (index >= len)//(2)
return false;
if (current[index] == o)//(3)
break findIndex;
index = indexOf(o, current, index, len);//(4)
if (index < 0)//(5)
return false;
}
//下面逻辑一样,创建一个新数组,引用指向新数组
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);
return true;
} finally {
lock.unlock();
}
}
findIndex: {}这个用法用的比较少,简单理解就是findIndex给"{}"里面的代码块做个标记,如果break findIndex;表示直接退出这个代码块,代码块里的代码不执行了,直接执行下面的逻辑。
不管是根据索引来删除元素还是直接删除元素,源码里面都是加了锁,创建了新数组来操作的。
这个代码块里,其实分析了被动过的数组的几种情况:
- 数组有删除,并且index位置的元素没有修改和删除,有删除的情况下,len和index有两种大小关系:
a. index>len,说明原先的数组[0,index]区间内肯定有删除(如果只有[index,len]区间内有删除,len不可能小于index)。那么代码走到1为ture,更新index,并将除了index位置的元素复制到新数组中返回
b. index<len,不用管[index,len]区间内有没有删除元素
ⅰ. 如果[0,index]区间内有删除元素,逻辑同a
ⅱ. 如果[0,index]区间内没有删除元素,说明只有[index,len]区间内有删除元素,则只有将除index位置的元素之外的其他元素复制到新数组返回 - 数组有删除,并且index位置的元素被修改或删除。那么代码(1)为false,如果index>=len(代码2),直接返回false,如果index<len,重新定位元素o的位置(代码4),找不到,直接返回false(代码5)
- 数组没有删除,并且index位置的元素没有修改和删除。则只要将除index位置的元素之外的其他元素复制到新数组返回
- 数组没有删除,并且index位置的元素被修改,重新定位o的位置(代码4),找不到直接返回false(代码5)
get(int index)方法
public E get(int index) {
return get(getArray(), index);
}
get方法,没有加锁。
总结:
CopyOnWrite添加和删除的时候都是复制了一个新数组,在新数组上添加删除,而读操作则没有限制。
所以优点显而易见,高并发下读写不会抛出并发修改异常,读写分离。
缺点就是高并发下,刚添加的元素可能不会马上读到,刚删除的元素可能还能读到