前言
在前面的文章中,我们陆续介绍了concurrent包的各个类,包括几种锁的使用及其实现,并发辅助工具的使用及其实现,本篇开始,我们继续介绍concurrent包中的并发容器的使用及其实现机制。
本篇,我们先来看一下并发容器:CopyOnWriteArrayList。
CopyOnWriteArrayList介绍
CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。
其对并发场景下操作提供了更好的支持,其内部使用ReentrantLock进行并发的控制,它主要具有以下特性:
- 1、所有元素都存储在数组里面, 只有当数组进行remove, update时才在方法上加上ReentrantLock, 拷贝一份snapshot的数组, 只改变snapshot中的元素,最后再赋值到CopyOnWriteArrayList中。
- 2、所有的get方法只是获取数组对应下标上的元素(无需加锁控制)。
CopyOnWriteArrayList是典型的使用空间换时间的方式进行工作, 它主要适用于读多些少,并且数据内容变化比较少的场景(最好初始化时就进行加载数据到CopyOnWriteArrayList中)。
由于是采用“快照”的方式进行的存储,因此其内部数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出ConcurrentModificationException。
CopyOnWriteArrayList的构造函数与方法列表,及其使用方法与ArrayList基本一致,在这里不再过多介绍。
CopyOnWriteArrayList源码分析
CopyOnWriteArrayList内部采用了一个数组存储数据,使用ReentrantLock进行并发控制,只有当数组进行remove、 update时才进行加锁操作,其实现了List、RandomAccess、Cloneable接口。
我们首先来看一下add方法的实现:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//1、获取锁
lock.lock();
try {
//2、获取当前数组元素
Object[] elements = getArray();
//3、获取数组长度
int len = elements.length;
//4、复制旧数组数据到新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//5、将新元素添加到新数组的尾部
newElements[len] = e;
//6、将新数组更新为当前数组
setArray(newElements);
return true;
} finally {
//7、释放锁
lock.unlock();
}
}
新增一个元素的操作比较简单,具体可以参考注释内容,可以发现其实它与ArrayList的add方法不同的是,CopyOnWriteArrayList会在开始操作前,上一把锁,进行并发的控制,然后再操作数据的时候,会创建一个副本,对副本进行操作。
这样做的好处是当使用迭代器进行迭代数组元素的时候,由于引用的对象是副本,因此不会抛出ConcurrentModificationException异常,但是由于每次新增操作都会创建一个新的数组,空间有一定的损耗,因此CopyOnWriteArrayList是比较适合读多写少的场景。
我们再来看一下remove方法,按下标删除元素的实现:
public E remove(int index) {
final ReentrantLock lock = this.lock;
//1、获取锁
lock.lock();
try {
//2、获取当前数组元素
Object[] elements = getArray();
//3、获取数组长度
int len = elements.length;
//4、获取要删除的下标的值
E oldValue = get(elements, index);
int numMoved = len - index - 1;
//5、如果数组长度减删除元素位置-1等于0,说明删除的元素的位置在len-1上,直接拷贝原数组的前len-1个元素
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
Object[] newElements = new Object[len - 1];
//6、拷贝原数组0-index之间的元素(index 不拷贝)
System.arraycopy(elements, 0, newElements, 0, index);
//7、拷贝原数组index+1到末尾之间的元素 (index+1也进行拷贝)
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
//8、将新的数组设置为当前数组
setArray(newElements);
}
//9、返回旧值
return oldValue;
} finally {
//10、释放锁
lock.unlock();
}
}
remove方法的实现比较简单,具体可以参见代码注释,主要分为几个步骤:
- 1、获取锁
- 2、获取当前数组元素
- 3、获取数组长度
- 4、获取要删除的下标的值
- 5、如果数组长度减删除元素位置-1等于0,说明删除的元素的位置在len-1上,直接拷贝原数组的前len-1个元素
- 6、拷贝原数组0-index之间的元素(index 不拷贝)
- 7、拷贝原数组index+1到末尾之间的元素 (index+1也进行拷贝)
- 8、将新的数组设置为当前数组
- 9、返回旧值
- 10、释放锁
我们主要来看一下直接remove元素的方法的实现:
public boolean remove(Object o) {
Object[] snapshot = getArray();
//寻找要删除的元素的下标
int index = indexOf(o, snapshot, 0, snapshot.length);
//如果没找到,返回false,否则执行删除操作
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;
}
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
//1、获取锁
lock.lock();
try {
//2、获取当前数组
Object[] current = getArray();
int len = current.length;
//3、如果数组副本与当前副本不一致
//这里findIndex的作用是,当执行break findIndex的时候,整个流程会退出,即if (snapshot != current)体中的逻辑会不再执行
if (snapshot != current) findIndex: {
//4、从index,len中取出一个较小的值prefix
int prefix = Math.min(index, len);
//5、从current数组中的prefix前个元素中寻找元素o,找到后,将其所在的位置,赋给index,然后break流程
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
//6、如果index >= len,则说明元素o在另外的线程中已经被删除,直接return
if (index >= len)
return false;
//7、如果current[index] == o,则代表,index位置上的元素o还在那边,break流程
if (current[index] == o)
break findIndex;
//8、从current数组中再找一遍,如果在其他线程中被删除掉了,直接return false
index = indexOf(o, current, index, len);
if (index < 0)
return false;
}
Object[] newElements = new Object[len - 1];
//9、拷贝原数组0-index之间的元素(index 不拷贝)
System.arraycopy(current, 0, newElements, 0, index);
//10、拷贝原数组index+1到末尾之间的元素 (index+1也进行拷贝)
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
//11、将新的数组设置为当前数组
setArray(newElements);
return true;
} finally {
//12、释放锁
lock.unlock();
}
}
上面的代码就是直接remove元素的实现,操作有点多,具体可以参见注释,其实可以看到,CopyOnWriteArrayList在remove元素时,进行了大量的线程之间的容错控制,防止多线程操作下出现问题。
总结
本篇我们介绍了CopyOnWriteArrayList的使用及其实现,其大体实现方式与ArrayList基本一致,对于部分方法提供了线程安全的支持。
我们来总结一下它的特性,它是使用了空间换时间的方式进行的实现,主要适用于读多些少,并且数据内容变化比较少的场景,例如白名单、黑名单等等。
下篇我们将会对ConcurrentHashMap的实现进行分析,敬请期待
更多Java干货文章请关注我的个人微信公众号:老宣与你聊Java