CopyOnWriteArrayList源码解析
一、简介
CopyOnWriteArrayList通过读写分离的形式重构ArrayList,保证ArrayList在循环遍历过程中的读写分离性,保证数组的最终一致性,适用于多读少写的情景下。
二、继承体系
![CopyOnWrite继承体系](
)
CopyOnWriteArrayList实现了List,Serializable,RandomAccess,Cloneable接口
实现List接口,为提供add,remove,get,set等操作
实现RandomAcess接口表示该类可自由访问
和ArrayList的继承相对比发现COWA和其先辈实现的接口基本一致,其中最本质的是Collection和List,保证这两个类能够在轻易的进行切换(面向对象的多态性)。
三、重要属性
/** The lock protecting all mutators */
//可重入锁,实现并发控制
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
//存储原始数组对象
private transient volatile Object[] array;
lock 实现写时加锁的控制
Object[] array存储需要的数据
四、重要方法解析
主要解析包括以下几个方法
4.1 构造函数
public CopyOnWriteArrayList(Collection<? extends E> c) {
//保存新的元素
Object[] elements;
//如果是同类型的就直接引用就行
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//如果数组类型不属于Object[].class 就重新赋值一份数组
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
两个方式的拷贝:第一种方式是如果是相同的直接进行引用即可,第二种方式通过重新赋值数组并且进行数组元素类型的转化来实现。
4.2 eq方法
private static boolean eq(Object o1, Object o2) {
return (o1 == null) ? o2 == null : o1.equals(o2);
}
用于判断两个对象是否相等,元素可能存在null,需要重写方法不能使用类似与o1.equals(o2)
的方式会导致NPE。
4.2 set方法
/** 替换指定index上的元素*/
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) {//使用==而不是equals()来判断
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics 保证写语义,这个不太明白什么意思
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();//归还锁
}
}
主要流程:
- 上锁
- 获取index上的对象
- 查看两个是否相等
- 不相等则copy新的数组
- 修改新的数组并修改引用
- 解锁
set方法为写方法,需要继续进行加锁,实现多个写之间的一致性。
记录index上的元素oldValue,如果两个地址是不完全一致的就复制原有数组并进行元素的修改,最后复制回去。
为什么使用
==
进行比较而不是eq(A,B)
array中实际存储的是对象数组,array不同于hash需要使用hashcode和equals函数来进行判断
4.3 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();
}
}
主要流程:
- 上锁
- 拷贝长度为len+1的新数组
- 进行复制
- 修改引用
- 解锁
从add中就能看出数组的长度变化情况,与ArrayList不同的扩容机制,COW的数组并不含有空余空间,数组完全饱和
4.4 add(int,E)方法
向指定位置上添加元素的方法
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)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;//右侧需要移动的元素个数
if (numMoved == 0)//到达最右侧,直接进行扩容
newElements = Arrays.copyOf(elements, len + 1);
else {//否则进行分段复制
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
主要流程:
- 上锁
- 确定右侧需要移动的元素
- 需要移动的元素为0
- 直接进行扩容
- 需要移动的元素不为0
- 进行数组扩容
- 分左右两侧进行复制
- 需要移动的元素为0
- 替换位置上的值
- 解锁
4.5 addIfAbsent(E)方法
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :/*先进性快速的判断*/
addIfAbsent(e, snapshot);
}
/**
* A version of addIfAbsent using the strong hint that given
* recent snapshot does not contain e.
*/
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))//判断公共的部分,如果该元素已经添加就不在添加
return false;
if (indexOf(e, current, common, len) >= 0)//判断剩余的部分
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
主要流程:
- 获取镜像进行,判断快照中是否存在元素e
- 如果存在返回false表示添加失败
- 如果不存在需要进行加锁操作
- 上锁
- 将现有元素与快照的公共部分(前半部分)进行比较,如果发现元素e已经被添加到数组中就结束
- 对后半部分进行indexOf查找操作,如果发现元素e就结束
- 进行扩容并添加新数据到结尾
- 解锁
其中有很多非常有意思的小细节
第一个:先进行了无锁化的查找,看是否存在元素,当不存在时再添加。而不是直接进行上锁在判断元素是否存在
第二个:正是由于上面的无锁化操作,导致快照和当前数组可能不一致,但依然利用上了快照信息,其中有个比较有意思的问题是没有直接使用indexOf进行重新查找,而是附加了比较查找,这里有个很底层的问题就是==
和!=
的比较速度要比indexOf中的eq()的速度快得多,加之对于Array大部分的操作都是add
或者add(index,e)
,如果index
的值较大的话对于效率的提升会更加高。
这里addIfAbsent(index,e)需要两步:确认是否存在和添加,将第一步的查找不加锁,而第二步修改进行加锁,实属精髓