基本概念
CopyOnWriteArrayList 的实现原理实际与 ArrayList 大同小异。它们内部都通过维护一个数组来对数据进行操作。
不同的是它的处理写操作(包括 add、set、remove)是先将原始的数据通过Arrays.copyof 方法复制生成一份新的数组。然后在新的数据对象上进行写操作,写完后再将原来的引用指向到当前这个数据对象。这样保证了每次写都是在新的对象上。而 ArrayList 除非是内部数组需要扩容操作才会复制生成一份新的数组。
为了保证写操作的一致性,这里要对各种写操作要加一把重入锁(ReentrantLock)来控制。
内部构造
- 成员变量,在 CopyOnWriteArrayList 中使用 array 表示它内部维护的数组 ,该数组用于存放数据。
private volatile transient Object[] array;
final void setArray(Object[] a) {
array = a;
}
final Object[] getArray() {
return array;
}
- 构造函数,CopyOnWriteArrayList 在被构造时,默认会创建一个容量为 0 的数组。值得注意的是,数组容量是固定的,但是对该类的每个操作都会通过复制创建新的数组,并且能在复制过程完成数组的容量变更。
public CopyOnWriteArrayList() {
// 关键 -> 为 0 是因为对数组的操作都会通过复制创建新的数组,一开始不用确定数组容量
setArray(new Object[0]);
}
原理分析
1.添加操作
在 CopyOnWriteArrayList 类中,存在两种添加操作:指定位置、不指定位置。
添加操作使用该类内部的 ReentrantLock 来控制并发,该锁是独占锁,说明同一时间有且只有一个线程能对该类执行该操作。
单个元素的添加操作默认会创建(容量+1)的新数组,并且从旧数组复制所有元素。由于这个特性,导致了该类的写操作需要大面积复制数组,所以性能肯定很差。
下面来看两种操作的实现过程:
- 添加元素时不指定位置
// 内部维护一个重入锁。
transient final ReentrantLock lock = new ReentrantLock();
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);
// 添加到数组末尾;数组容量为 len+1 时,下标为 0 - len。
newElements[len] = e;
// 更新内部数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
- 添加元素时指定位置
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){
// 抛出异常...
}
Object[] newElements;
// 需要移动的元素,为 0 表示直接放置在数组末尾
int numMoved = len - index;
if (numMoved == 0){
newElements = Arrays.copyOf(elements, len + 1);
}else {
newElements = new Object[len + 1];
// 复制 [0-index] 的元素到新数组的[0-index]
System.arraycopy(elements, 0, newElements, 0, index);
// 复制 [index-末尾]的元素到新数组的 [(index+1)-末尾]
// 空出 index 位置用来添加新元素
System.arraycopy(elements, index, newElements, index + 1, numMoved);
}
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
2.查询操作
在 CopyOnWriteArrayList 类中,存在两种查询操作:查询指定位置的元素、查询指定的元素。
后者比前者多了一步查询元素位置的过程。
需要注意的是:在该类中只有查询操作不需要加锁。
下面来看实现过程:
- 查询指定位置的元素
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
- 查询指定的元素
public boolean contains(Object o) {
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length) >= 0;
}
private static int indexOf(Object o, Object[] elements, int index, int fence) {
// 区分是不是 null,该方法的原理与 Arraylist 一样。
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;
}
3.修改操作
需要注意的是,虽然修改操作不改变数组的容量,但也会创建新的数组并从旧数组复制元素。
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
// 与该位置上的旧值比对
if (oldValue != element) {
int len = elements.length;
// 复制新的数组对象,再替换元素
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
4.删除操作
该类的删除操作分为:删除指定位置的元素、删除指定元素,但是实现过程却大相径庭。下面来看实现过程:
- 删除指定位置的元素
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 {
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 remove(Object o) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (len != 0) {
int newlen = len - 1;
Object[] newElements = new Object[newlen];
// ①比较 [0 - (newlen -1 = len -2)] 位置上的元素
for (int i = 0; i < newlen; ++i) {
// 比较 o1,o2
if (eq(o, elements[i])) {
// 相等,则通过复制,将元素前移,覆盖掉 i 位置上的元素。
for (int k = i + 1; k < len; ++k){
newElements[k - 1] = elements[k];
}
setArray(newElements);
return true;
} else{
// 不相等执行复制操作
newElements[i] = elements[i];
}
}
// ②比较 [newlen = len -1]位置上的元素,说明指定元素在数组末尾
if (eq(o, elements[newlen])) {
setArray(newElements);
return true;
}
}
return false;
} finally {
lock.unlock();
}
}
// 比较 01,02 是否相等
private static boolean eq(Object o1, Object o2) {
return (o1 == null ? o2 == null : o1.equals(o2));
}
总结
在 CopyOnWriteArrayList 中添加、修改、删除都属于写操作,查询属于读操作。关于读写问题,具有以下特性:
写操作:需要大面积复制数组,而且需要锁保证数据的一致性,所以性能肯定很差。
读操作:因为操作的对象和写操作不是同一个对象,而且读操作的线程之间也不需要加锁。
读写同步:读和写之间的同步处理只是在写完后通过一个简单的 = 将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合在多线程里使用,绝对不会发生 ConcurrentModificationException 。
综上所述, CopyOnWriteArrayList 适合使用在读操作远远大于写操作的场景里,比如缓存。