CopyOnWriteArrayList是线程安全的ArrayList,本文基于JDK1.8对CopyOnWriteArrayList源码分析。
1.类结构
CopyOnWriteArrayList类层级结构图:
CopyOnWriteArrayList实现了List的所有方法 ,主要成员变量有以下两个:
//可重入锁,用于对写操作加锁
final transient ReentrantLock lock = new ReentrantLock();
//Object对象数据,用来存储数据,使用volatile关键字修饰,目的是一个线程对字段的修改使另一个线程立即可见
private transient volatile Object[] array;
可以看到CopyOnWriteArrayList没有和容量有关的属性或者常量,下面根据源码进一步分析,就可以知道原因了。
2.方法解析
2.1 构造函数
CopyOnWriteArrayList() 无参构造函数
public CopyOnWriteArrayList() {
//无参构造方法,创建一个长度为0的数组
setArray(new Object[0]);
}
无参构造函数创建了一个长度为0的对象数组
CopyOnWriteArrayList(Collection<? extends E> c)
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
//指定集合c的集合类型就是CopyOnWriteArrayList,则直接强转为CopyOnWriteArrayList再调用getArray()方法,赋值给elements
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
//将集合转为对象数组
elements = c.toArray();
if (c.getClass() != ArrayList.class)
//指定集合c的集合类型不是ArrayList(ArrayList底层是对象数组),就将对象数组拷贝复制到elements
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
//设置array值
setArray(elements);
}
CopyOnWriteArrayList(E[] toCopyIn)
public CopyOnWriteArrayList(E[] toCopyIn) {
//将入参拷贝一份赋值给array
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
2.2 add(E e)
add(E e)往CopyOnWriteArrayList末尾添加元素
public boolean add(E e) {
//获取可重入锁
final ReentrantLock lock = this.lock;
//加锁,同一时间只能有一个线程进入
lock.lock();
try {
//获取当前数组array
Object[] elements = getArray();
//获取当前array数组长度
int len = elements.length;
//复制一个新数组,新数组长度为当前array数组的长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新数组末尾添加元素
newElements[len] = e;
//将新数组赋值给array属性
setArray(newElements);
return true;
} finally {
//释放锁
lock.unlock();
}
}
add操作使用ReentrantLock可重入锁来确保线程安全。通过add方法,我们可以看出CopyOnWriteArrayList修改操作的基本思想为:
a.复制一个新数组,新数组长度为当前数组长度+1,刚好能够容纳要添加的元素
b.在新数组里操作(添加、修改或者删除)
c.将新数组赋值给array属性,替换旧数组
这种思想称为“写时复制”,所以叫做CopyOnWriteArrayList。
此外,CopyOnWriteArrayList没有类似ArrayList中的grow()方法扩容的操作
2.3 add(int index, E element)
add(int index, E element)在指定下标添加指定元素
public void add(int index, E element) {
//获取可重入锁
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//获取当前数组array
Object[] elements = getArray();
//获取当前数组array的长度
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)
//当numMoved=0,说明是在末尾添加,和add(E e)方法一致
newElements = Arrays.copyOf(elements, len + 1);
else {
//否则创建一个新数组,新数组长度为当前array数组的长度+1
newElements = new Object[len + 1];
//将原数组index之前的元素复制到新数组
System.arraycopy(elements, 0, newElements, 0, index);
//将原数组index下标后所有(共numMoved个)元素复制到新数组
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
//将新数组的index位置设置为指定元素element
newElements[index] = element;
//将新数组赋值给array属性
setArray(newElements);
} finally {
//释放锁
lock.unlock();
}
}
2.4 remove(int index)
remove(int index)删除指定下标元素
public E remove(int index) {
//获取可重入锁
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//获取当前数组array
Object[] elements = getArray();
//获取当前数组array的长度
int len = elements.length;
//获取array数组index下标的旧值
E oldValue = get(elements, index);
//计算需要移动的元素个数
int numMoved = len - index - 1;
if (numMoved == 0)
//如果删除的是最后一个元素,将当前array设置为新数组,新数组的长度为旧数组长度-1
setArray(Arrays.copyOf(elements, len - 1));
else {
//创建一个新数组,数组长度为旧数组长度-1
Object[] newElements = new Object[len - 1];
//分段复制,将原数组index之前的元素复制到新数组
System.arraycopy(elements, 0, newElements, 0, index);
//分段复制,将原数组index+1之后的元素复制到新数组
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
//将新数组赋值给array属性
setArray(newElements);
}
//返回旧值
return oldValue;
} finally {
//释放锁
lock.unlock();
}
}
通过代码可以发现,CopyOnWriteArrayList中的增删改操作都是在新数组中进行的,通过加锁的方式确保同一时刻只有一个线程可以操作,操作后赋值给array属性,替换旧数组。
2.5 remove(Object o)
remove(Object o)删除指定元素
public boolean remove(Object o) {
//获取当前数组array
Object[] snapshot = getArray();
//获取对象o的下标
int index = indexOf(o, snapshot, 0, snapshot.length);
//如果index<0,表示数组中不存在对象o,返回false,否则调用remove()方法删除对象
return (index < 0) ? false : remove(o, snapshot, index);
}
private boolean remove(Object o, Object[] snapshot, int index) {
//获取可重入锁
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//获取当前数组array
Object[] current = getArray();
//获取当前数组array的长度
int len = current.length;
//如果入参数组snapshot和当前数组current 不相等,说明当前数组已经被其他线程修改
if (snapshot != current)
//定义一个循环体,设置别名为:findIndex,使用break findIndex可以跳出(多重循环)循环体
findIndex: {
//在指定的下标index和当前数组array的长度 取最小值
int prefix = Math.min(index, len);
for (int i = 0; i < prefix; i++) {
//集合结构发生修改,且元素未被删除
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
//待删除的元素下标index大于数组长度len,则该元素已经被其他线程删除,方法结束返回false
if (index >= len)
return false;
//指定下标index元素值和待删除的元素值匹配,直接退出循环体
if (current[index] == o)
break findIndex;
//遍历当前数组index下标之后的部分,获取指定元素o的下标
index = indexOf(o, current, index, len);
//若获取不到,方法结束,返回false
if (index < 0)
return false;
}
//创建新数组,新数组的长度为当前数组长度的len-1
Object[] newElements = new Object[len - 1];
//分段复制,将原数组index之前的元素复制到新数组
System.arraycopy(current, 0, newElements, 0, index);
//分段复制,将原数组index+1之后的元素复制到新数组
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
//将新数组赋值给array属性
setArray(newElements);
return true;
} finally {
//释放锁
lock.unlock();
}
}
可以看到,remove(Object o)方法没有加锁,实际上是调用的方法remove(Object o, Object[] snapshot, int index) 加锁了。可能导致在调用remove(Object o)方法之后到获取到锁之前,当前数组array被其他线程修改,所以要判断入参snapshot 和当前数组current是否相等。
知识点:
a.break只可跳出一层循环,给break 设置别名,可用于跳出多重循环。语法格式为:
for (int i = 0; i < 3; i++) {
flag: for (int j = 0; j < 5; j++) {
if (j == 3) {
break flag;
}
System.out.print("a");
}
System.out.println("b");
}
#输出如下:
aaab
aaab
aaab
当j=3时,直接跳出了if和内层for循环
b.分段复制
//分段复制,将原数组index之前的元素复制到新数组
System.arraycopy(elements, 0, newElements, 0, index);
//分段复制,将原数组index+1之后的元素复制到新数组
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
2.6 set(int index, E element)
set(int index, E element) 设置指定位置的值
public E set(int index, E element) {
//获取可重入锁
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//获取当前数组array
Object[] elements = getArray();
//获取指定下标index的旧值
E oldValue = get(elements, index);
if (oldValue != element) {
//如果旧值和新值不相等,获取当前数组array的长度
int len = elements.length;
//复制一个新数组,长度和旧数组一致
Object[] newElements = Arrays.copyOf(elements, len);
//为新数组index下标赋值为新值
newElements[index] = element;
//新数组赋值给array属性,替换旧数组
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
//即使新值和旧值一致,为了确保volatile语义,需要重新设置array 目的:因为array属性被volatile修改,防止指令重排序
setArray(elements);
}
//返回旧值
return oldValue;
} finally {
//释放锁
lock.unlock();
}
}
可以看到,set操作时,当新值和指定下标index的旧值相等,仍然需要重新设置array,这是因为数组array被volatile修饰,设置后可保证代码的执行前后顺序,防止指令重排。后续再单独分析volatile关键字的原理。
2.7 get(int index)
get(int index)获取指定下标的元素
public E get(int index) {
//调用getArray()获取当前数组array,
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
//获取数组a 下标index的值
return (E) a[index];
}
可以看到get方法没有加锁,所以在并发情况下可能出现以下情况:
a.线程1调用get(int index)方法取值,内部通过getArray()方法获取到array属性值
b.线程2调用CopyOnWriteArrayList的增删改方法,内部通过setArray()方法在新数组中修改了值,还未赋值给array属性,替换旧数组
c.线程1还是从旧的array数组中取值
所以get方法是弱一致性的。
2.8 size()
public int size() {
//获取数组元素个数
return getArray().length;
}
size()返回当前array数组长度,因为CopyOnWriteArrayList中的array数组每次当刚好能容纳下所有元素,并不像ArrayList那样会预留一定的空间。所以CopyOnWriteArrayList没有size属性,元素个数和数组长度是相等的。
2.9 迭代器
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the 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;
}
......
}
可以看到,迭代器没有在锁中进行,也是弱一致性的。如果没有其他线程对CopyOnWriteArrayList进行增删改操作,那么snapshot还是创建迭代器时获取的array。如果有其他线程对CopyOnWriteArrayList进行增删改操作,旧的数组会被新数组给替换掉,但是snapshot还是原来的旧的数组的引用:
CopyOnWriteArrayList<String> tempList = new CopyOnWriteArrayList<>();
tempList.add("hello");
Iterator<String> iterator = tempList.iterator();
tempList.add("world");
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
# 输出结果为hello
3. 总结
a.CopyOnWriteArrayList体现了写时复制的思想,增删改都是在复制的新数组中操作的
b.CopyOnWriteArrayList的增删改方法通过ReentrantLock可重入锁确保线程安全
c.CopyOnWriteArrayList的取值方法是弱一致性
d.同一时刻只能有一个线程对CopyOnWriteArrayList进行增删改操作,而读操作没有限制,并且
CopyOnWriteArrayList的增删改都需要复制一个新数组,增加了内存的消耗,所以CopyOnWriteArrayList适合读多写少的情况