一、概述
CopyOnWriteArrayList是对List进行操作的一个线程安全的类,是ArrayList的线程安全的变体,Copy-On-Write一般翻译为“写入时复制”,很明显,该类实现线程安全的方式是通过复制来实现的,也就是说,首先提供一个不可变的对象,然后在每次修改时,都会创建并生成一个新的不可变的对象,从而实现可变性。
自然的,这种实现方式也有它的缺点,因为每当修改容器时都会复制底层数组,这需要一定的开销,特别是当容器的规模比较大时,所以只有当查询操作远远多于修改操作时,才建议使用该类。
本系列源码全是基于JDK 1.8的。
二、CopyOnWriteArrayList
1. 继承结构及构造方法
public class CopyOnWriteArrayList
implements List, RandomAccess, Cloneable, java.io.Serializable {
继承结构比较常规,可以随机访问(RandomAccess),可以复制(Cloneable),可以序列化(Serializable ),然后来看下构造方法:
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
public CopyOnWriteArrayList(Collection extends E> c) {
Object[] elements;
// 类型是否相同
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList>)c).getArray();
else {
// 不同,先转为数组,然后判断是否为Object数组类型
// 如果不是Object数组类型,就转为该类型
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
// 设置数组
setArray(elements);
}
public CopyOnWriteArrayList(E[] toCopyIn) {
// 转化为Object数组类型,然后设置数组
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
final void setArray(Object[] a) {
array = a;
}
由于该类底层同样是基于数组实现的,所以构造方法的操作也是围绕着数组进行的,该类总共提供了三个构造方法,其实都很简单,源码中也简单添加了注释,这里就不多说了。
2. 常用属性
这里也来简单看下用到的一些属性:
/** 使用可重入锁保证线程安全 */
final transient ReentrantLock lock = new ReentrantLock();
/** 数组,用于存放具体的元素 */
private transient volatile Object[] array;
/** Unsafe变量,用于实现原子性操作 */
private static final sun.misc.Unsafe UNSAFE;
private static final long lockOffset;
3. 相关内部类
3.1 COWIterator类
static final class COWIterator implements ListIterator {
/** 基础数组array的快照 */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
/** 可以翻译为游标 */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
/** 不支持add,set,remove操作 */
public void remove() {
throw new UnsupportedOperationException();
}
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
@Override
public void forEachRemaining(Consumer super E> action) {
Objects.requireNonNull(action);
Object[] elements = snapshot;
final int size = elements.length;
for (int i = cursor; i < size; i++) {
@SuppressWarnings("unchecked") E e = (E) elements[i];
action.accept(e);
}
cursor = size;
}
}
该类用于表示迭代器,提供了一个基础数组array的快照,该迭代器保留了一个指向底层基础数组的引用,该数组不会被修改,因此在对其进行操作的时候只需确保数组内容的可见性,这样多个线程可以同时对这个容器进行迭代,而不会发生冲突。
4. 方法
接下来,我们来看一下它常用的一些方法,先看下add方法:
4.1 add方法
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 进行加锁操作
lock.lock();
try {
// 获取基础数组array
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();
}
}
add方法很简单:
- 首先,获取锁,获取数组及数组长度;
- 其次,在原数组基础上复制一个新的数组,新的数组比原数组长度多1;
- 然后把新的元素添加到数组的尾部;
- 最后把该数组赋值给基础数组array;
而add的另一个重载方法就稍微复杂一点点:
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;
// 如果元素是数组最后一个元素,操作和上面描述的一个参数的add方法操作相同
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
// 否则,构造新的数组,长度是 基础数组长度加1
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();
}
}
其实该方法也比较简单,主要是判断了下index的大小,然后根据index的大小进行相应的复制操作,这里注释已经很清晰了,这里就步多说了。
4.2 addIfAbsent方法
该方法表示添加的时候如果数组中不存在,则进行添加;如果存在,则不添加,直接返回:
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
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++)
// 如果当前数组元素与快照数组元素不相等,并且要添加的元素与当前数组元素相等
// 说明快照与当前current之间数组发生了修改,并且设置了数组某一元素为e,已经存在,直接返回
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();
}
}
这个方法我们来看下流程:
- 首先还是相同的操作,获取锁,获取当前数组,获取数组长度;
- 然后判断原先的快照数组和当前的数组是否相等,不相等则说明数组发生了修改;
- 这个时候取数组长度较小的值,进行遍历操作;
- 如果当前数组元素与快照数组元素不相等,并且要添加的元素与当前数组元素相等,说明快照与当前数组current之间,数组发生了修改,并且设置了数组某一元素为e,说明已经存在,直接返回;
- 如果上述条件没有发生,则从下标common开始再次进行查找操作;
- 如果查找不到,进行后续的添加操作;
4.3 indexof方法
public int indexOf(Object o) {
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length);
}
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
// 如果要查找的元素是null
if (o == null) {
for (int i = index; i < fence; i++)
// 如果有值是null的直接返回
if (elements[i] == null)
return i;
} else {
// 遍历,根据equals方法进行判断
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
indexof方法表示从前往后查找该元素所在的第一个下标值,而与之相对应的则是lastIndexOf方法,这里也不多说了。
4.4 set方法
同样,set方法也很简单,我们来看一下:
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
// 先获取index处的元素值
E oldValue = get(elements, index);
// 比较下旧的值和新的值是否相同
if (oldValue != element) {
// 不相同,复制一个新的数组,并设置对应index处的值为新的值
// 再更新数组
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();
}
}
4.5 Array.copyof方法
CopyOnWriteArrayList这个类的操作基本都是通过Array.copyof
方法来实现的,我们顺便来看下这个方法的源码:
public static T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
public static T[] copyOf(U[] original, int newLength, Class extends T[]> newType) {
@SuppressWarnings("unchecked")
// TODO,这里没看太懂,等以后来补充
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
可以看到,该方法最终是通过系统类System的底层方法arraycopy
来实现的。由于方法内部的操作都差不多,所以看了这几个方法的源码后,其他方法的源码就不用看了,比如说remove方法,和重载的add方法操作是类似的。
三、CopyOnWriteArraySet
最后,再来简单看下CopyOnWriteArraySet这个类,这个类就更简单了,底层封装了一个CopyOnWriteArrayList对象,所有对该Set的操作,实际上都是通过对CopyOnWriteArrayList来操作的。同样,该类和CopyOnWriteArrayList有相同的一些特性,比如适用于当查询操作远远多于修改操作的情况;
该类继承了AbstractSet类,从而可以最大程度的减少Set接口的实现;而CopyOnWriteArraySet实现元素的不重复,则是借助于CopyOnWriteArrayList中的addIfAbsent
和addAllAbsent
方法来实现的。比如:
public class CopyOnWriteArraySet extends AbstractSet
implements java.io.Serializable {
private static final long serialVersionUID = 5457747651344034263L;
private final CopyOnWriteArrayList al;
public CopyOnWriteArraySet(Collection extends E> c) {
if (c.getClass() == CopyOnWriteArraySet.class) {
@SuppressWarnings("unchecked") CopyOnWriteArraySet cc =
(CopyOnWriteArraySet)c;
al = new CopyOnWriteArrayList(cc.al);
}
else {
al = new CopyOnWriteArrayList();
al.addAllAbsent(c);
}
}
public boolean add(E e) {
return al.addIfAbsent(e);
}
}
四、总结
CopyOnWriteArrayList的源码其实特别简单,该类的实现就是围绕着复制来实现的,也就是围绕着Arrays.copyOf
方法来实现的,这个方法了解了,该类的核心思想也就理解了。而CopyOnWriteArraySet则更简单了,只是封装了一个CopyOnWriteArrayList,然后对这个List进行操作即可。另外,Arrays.copyof
方法还有点没看明白,等哪天参考下某位大神的博客后,再来补充下。
本文参考自:
《Java并发编程实战》
【JUC】JDK1.8源码分析之CopyOnWriteArrayList(六)