CopyOnWriteArrayList解决了ArrayList并发安全性问题,其特性是在修改结构时(remove/add/clear等)会使用ReentrantLock加锁,创建一个新数组来操作,然后对老数组进行合并覆盖(根据具体方法来看),而在读取元素时,始终读取的是原数组的值,实现了共享读,排斥写的效果。
CopyOnWriteArrayList的get方法:
private transient volatile Object[] array;
final Object[] getArray() {
return array; //array是实际存储元素的数组
}
private E get(Object[] a, int index) {
return (E) a[index]; //返回数组a索引位index的元素
}
public E get(int index) {
return get(getArray(), index); //返回array数组索引位index的元素
}
可以看到,get方法并没有加锁,因此在对集合进行get操作时始终读的是array的元素,而且没有加锁。
CopyOnWriteArrayList的add方法:
final transient ReentrantLock lock = new ReentrantLock();
final void setArray(Object[] a) {
array = a;
}
public boolean add(E e) {
final ReentrantLock lock = this.lock; //获取锁
lock.lock(); //加锁
try {
Object[] elements = getArray(); //通过getArray()获取老数组
int len = elements.length; //拿到老数组的长度
//使用Arrays.copyOf方法基于老数组创建新数组,长度为老数组的长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e; //直接将新元素添加到新数组的末尾
setArray(newElements); //将老数组覆盖
return true;
} finally {
lock.unlock(); //锁的正确释放
}
}
CopyOnWriteArrayList的remove方法:
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;
if (numMoved == 0)
//若需要移动的元素个数为0,则说明删除的是最后一个元素,因此只需要保留len-1位元素
setArray(Arrays.copyOf(elements, len - 1));
else {
//若需要移动的元素个数不为0,说明删除的元素不是最后一个,因此需要分开copy
//首先创建一个新数组newElements,长度为老数组长度-1
Object[] newElements = new Object[len - 1];
//先将老数组在index之前的所有元素copy到新数组
System.arraycopy(elements, 0, newElements, 0, index);
//再将老数组在index之后的所有元素copy到新数组
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue; //返回删除的元素
} finally {
lock.unlock();
}
}
add方法与remove方法都使底层存放元素的数组array保持在一个理想的容量,即有多少个元素,数组的长度就有多长,因此我们在阅读源码时也可以发现,CopyOnWriteArrayList并没有像ArrayList一样提供类似以下初始化集合容量的构造方法:
public ArrayList(int initialCapacity) { //ArrayList初始化容量构造方法
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
并且也没有类似以下的扩容方法:
private void grow(int minCapacity) { //ArrayList gorw方法
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
CopyOnWriteArrayList在保证了并发安全的同时,也带来了较大的开销,因为在每一次变动时,都需要基于原数组copy一份新数组出来,这无疑是非常昂贵的,因此在修改操作频繁时,非常不建议使用该类,但若是修改操作不频繁并且读取操作频繁时,使用该类还是很划算的。总之具体是否该使用该类来作为线程安全的集合使用,需要结合实际的应用场景来看。
最后感谢观看,若有不到位或错误的地方,还请大佬们在评论区中指出。