CopyOnWriteArrayList 原理:
先通过名字定义来看,“在写时复制的列表” 其原理也是如名字含义显而易见。 先看几个着重点:
1、线程安全
2、适合多读少写场景
3、弱一致性
4、迭代器不支持可变操作【add,set,remove】
大家先把这4点留个印象在脑海里,带着这些点,咱们通过源码跟踪【add,set,indexOf ,remove】进行逐一证实上述观点。
/** 互斥锁 */
final transient ReentrantLock lock = new ReentrantLock();
/** 数组*/
private transient volatile Object[] array;
/**
* 初始化一个数组长度为0的列表
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
/**
* 通过集合方式初始化
*/
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)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
/**
* 通过固定数组初始化
*/
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
public E set(int index, E element) {
//获取锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//get 当前数据
Object[] elements = getArray();
//获取指定下标对应的数组值
E oldValue = get(elements, index);
//如上述获取不为空.
if (oldValue != element) {
//获取当前数组的总长度
int len = elements.length;
//注意这里是并发的安全的要点
//会将原来old的数组复制出来,在新的数组上,进行oldVal的替换
Object[] newElements = Arrays.copyOf(elements, len);
//基于新数组进行值替换
newElements[index] = element;
//最后数组的转移至新的数组
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
//如果指定的index没有数据,那么不做任何处理.
setArray(elements);
}
//返回老的数据
return oldValue;
} finally {
//解锁
lock.unlock();
}
}
public boolean add(E e) {
//加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取当前数组
Object[] elements = getArray();
//数组长度
int len = elements.length;
//基于old数组长度上 + 1 复制一个新的数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
/**
* 在新数组的尾部赋予新值
* 在这会有不会有人看迷糊? 为啥上面用len作为新值得下标?? 简单提下:
*
* 因为数组存值是从0开始,那么也就意味着总长度,
* 始终是数组的最后一个值得下标:总长度-1, 所以上面进行了len + 1 正好就补充进来了.
*
* 例如 先数组有 1 2 3 4值. len = 4
* 那么各个值对应的下标 : 1=0 2=1 3=2 4=3
* 最后的4 的下标是 len - 1, 这时我在add 5 进来,
* 此时需要把len + 1增大数组的容量, 那么Newlen = 5了, Oldlen = 4也就是5的在数组对应的位置了.
*
*/
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;
//如果指定的下标,大于数组长度,或者下标<0 那么抛出越界异常
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
//预先声明新数组
Object[] newElements;
/**
* 划重点: 为什么常说list不适合频繁插入和删除操作?
*
* numMoved = 数组长度 - 指定下标 = 移动的长度
*
* 1. 如果numMoved = 0 那么说明指定的index 与 数组长度是一致的,其实就是说在数组的尾部添加值.就是add(E e)的逻辑
* 在原数组的长度上 + 1进行对原数组的copy ,并把值赋值到尾部
*
* 2. 如果numMoved > 0,那么先声明新数组,在原数组基础扩大一位,
* 2.1 从原数组的首位开始,copy index数量的数据到新数组中,从首位开始赋值
* 2.2 从原数组的第index位置开始,copy 前面计算的移动长度numMoved 个数据到新数组中,从index + 1的位置开始赋值
* 2.3 因此新数组的第index的下标就空出了,在将val值赋值
*/
int numMoved = len - index;
if (numMoved == 0)//扩大一位,赋值
newElements = Arrays.copyOf(elements, len + 1);
else {
//声明len + 1的数组
newElements = new Object[len + 1];
//从原数组的 第0下标开始 copy index个数据 之新数组,从0下标开始
System.arraycopy(elements, 0, newElements, 0, index);
//从原数组的 第index下标开始,copy numMoved个数据 之新数组,从index + 1下标开始
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
//将index 赋值val
newElements[index] = element;
setArray(newElements);
} finally {
//解锁
lock.unlock();
}
}
以上介绍了三种写入数据的方法,那么由此可以看出不论是set的替换数据,以及是在数据尾部或者指定位置插入数据,均有一个Arrays.copyOf(elements, len) 或者是System.arraycopy(elements, 0, newElements, 0, index)。 那么这个是干啥的呢?
这就是对应 写时复制的思想, 在对数组进行可变操作时,都先在原数组的基础上copy出来,在新的数组上,进行相对应的可变操作,操作完毕后,在将新数组指向到全局volatile Object[] array。
so 所以每一次的可变操作,都得进行一个新数组的生成,如果是在指定的位置操作,那么还得将指定位置后面的所有数据,一一往后一位移动。 这个性能就不言而喻了。 所以这就是为什么CopyOnWriteArrayList只适合多读少写的场景
那么写入 是咋保证线程安全的呢? ----------可以看到容器这块君都使用互斥锁ReentrantLock#lock(); 方式帮我们进行控制,因此在写入时不必担心线程安全;
杂一看我去,为啥get()方法实现如此之简单,用什么保证读安全的? 其实在我们了解了上述CopyOnWriteArrayList写入的机制后,再来看读方法,你是不是已经有眉目? 先来看下简单的源码:
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
/**
* 获取指定val的下标。
index
*/
public int indexOf(E e, int index) {
Object[] elements = getArray();
return indexOf(e, elements, index, elements.length);
}
由源码的知道,在读取数据时,直接是通过后获取数组的下标进行读取, 那么保证读取安全的第一个要点:
1 、 private transient volatile Object[] array; 使用volatile修饰,保证可见性,有序性;
2、其次是在进行可变操作时, 都先预备新的数组,操作完成后在进行主内存中新老数组替换,通过volatile保证被修改后,所有线程看到数组是最新的。
那么也由此可见 该容器是弱一致性 为什么这说呢?
可能在某一时刻,在指定位置插入时, 新的数组已经生成,但还在进行老数组的copy中,此时有线程来读取数据了,那么这时读取的数组 与 最终的数组就不是一致的。
remove方法也是属于可变操作中一种,因此也是采用写时复制的思路。
E remove(int index) 源码如下:
public E remove(int index) {
//加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取当前数组
Object[] elements = getArray();
//过去当前数组长度
int len = elements.length;
//获取指定下标的val值
E oldValue = get(elements, index);
/**
* 这里重点分析下numMoved三种情况
*
* 1 numMoved = 0 说明移除的最后一个数据. 所以直接把 len-1后的老数组copy过去即可.
*
* 2 numMoved < 0 说明已经越界了,数组里面并没有,指定index. 在E oldValue = get(elements, index);就抛出越界异常了.
*
* 3 numMoved > 0 说明需要移除的是非最后一位,那么就要为找index这个位置进行0-index-1的转移,以及index-1 - 尾部的数据转移
*
*/
int numMoved = len - index - 1;
if (numMoved == 0)
//移除最后一位,直接在原数组上 进行len-1操作即可.
setArray(Arrays.copyOf(elements, len - 1));
else {
//移除非最后一位,那么先声明一个len -1的新数组
Object[] newElements = new Object[len - 1];
//从old数组的第0位开始,转移index个数据到新数组中,也是从0开始
System.arraycopy(elements, 0, newElements, 0, index);
//从old数组的第index+1位开始,转移numMoved个数据到新数组中,也是从index开始
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
//最后指向新的数组
setArray(newElements);
}
//返回移除掉的val值
return oldValue;
} finally {
//解锁
lock.unlock();
}
}
所以可以看出根据index移除数据直接使用ReentrantLock方式进行线程安全保护。这里保护有啥作用的? 当持有此锁时,其他可变操作就需要等待锁释放,那么当前数组是不会可变了,因此直接找到对应index移除即可;
经过上述方法解析,相信大家已经有锁了解了。 但是COWList也不是完美的,因为它的迭代器是无法支持可变操作【set,add,remove】。 但也没关系,是可以支持在for循环中使用,均是采用写时复制的思路, 所以由衷的建议大家,不要轻易操作可变操作。 如果数据非常大的情况下,进行可变操作,那么内存就是立马飙升,以至于频繁FullGc,严重可能导致OOM。 这样就得不偿失了。
其次读写是弱一致性,只能保证最终一致。 那么对于实时要求较高的场景也是不适用的。
可见如此作者设计该容器时,就是认为读要大于写的这么一种思想。 多用于固定数据下,并发读的场景。