CopyOnWriteArrayList源码解析

1.概览:

1.1简介:

​ CopyOnWriteArrayList和ArrayList的底层数据结构一样,也是个数组,采用volitale关键字修饰,一旦数组被更改,其他线程能立马被感知到。CopyOnWriteArrayList是一个写时复制的容器,对容器中的元素进行增删改时,不在当前容器上进行操作,而是复制一个新的的Object数组newElements,在新的容器内进行增删改。操作后再将原容器的引用指向新的容器 setArray(newElements);
​ 这样做的好处是可以对 CopyOnWriteArrayList 容器的进行并发的读,而不需要加锁,因为当前容器不会添加任何新的元素。
​ 所以说 CopyOnWriteArrayList是一种读写分离的思想,读和写在不同的容器。

1.2属性:

//用来给CopyOnWriteArrayList的写操作加锁
final transient ReentrantLock lock = new ReentrantLock();

//该数组用volitale关键字修饰,一旦数组被修改,其它线程立马能够感知到
private transient volatile Object[] array;

2.重要方法源码解析:

2.1修改方法:

修改制定索引的元素

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 {
//相同,就不进行修改。原容器的引用不变
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
  • 为啥要用volitale关键字修饰了,还要拷贝数组呢?
  1. volatile 关键字修饰的是数组,如果我们简单的在原来数组上修改其中某几个元素的值,是无法触发可见性的,我们必须通过修改数组的内存地址才行,也就说要对数组进行重新赋值才行。
  2. 在新的数组上进行拷贝,对老数组没有任何影响,只有新数组完全拷贝完成之后,外部才能访问到,降低了在赋值过程中,老数组数据变动的影响

2.2新增方法:

直接添加:

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);
//将新值放到数组的尾部
newElements[len] = e;
//将原容器的引用指向新数组
setArray(newElements);
//添加成功返回true
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)
throw new IndexOutOfBoundsException("Index: " + index +
", Size: " + len);

Object[] newElements;
// len:数组的长度、index:插入的位置
int numMoved = len - index;
// 如果要插入的位置正好等于数组的末尾,直接拷贝数组即可
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
// 如果要插入的位置在数组的中间,就需要拷贝 2 次
// 第一次从 0 拷贝到 index。
// 第二次从 index+1 拷贝到末尾。
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
// index 索引位置的值是空的,直接赋值即可。
newElements[index] = element;
// 把新数组的值赋值给数组的容器中
setArray(newElements);
} finally {
lock.unlock();
}
}

从 add 系列方法可以看出,CopyOnWriteArrayList 通过加锁 + 数组拷贝+ volatile 来保证了线程安全,每一个要素都有着其独特的含义:

  1. 加锁:保证同一时刻数组只能被一个线程操作;
  2. 数组拷贝:保证数组的内存地址被修改,修改后触发 volatile 的可见性,其它线程可以立马知道数组已经被修改;
  3. volatile:值被修改后,其它线程能够立马感知最新值。

2.3删除方法:

删除某个索引位置的数据

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 {
// 如果删除的数据在数组的中间,分三步走
// 1. 设置新数组的长度减一,因为是减少一个元素
// 2. 从 0 拷贝到数组新位置
// 3. 从新位置拷贝到数组尾部
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) {
Object[] snapshot = getArray();
int index = indexOf(o, snapshot, 0, snapshot.length);
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 {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) findIndex: {
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;
}
}
if (index >= len)
return false;
if (current[index] == o)
break findIndex;
index = indexOf(o, current, index, len);
if (index < 0)
return false;
}
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

批量删除包含在 c 中的元素

我们并不会直接对数组中的元素进行挨个删除,而是先对数组中的值进行循环判断,把我们不需要删除的数据放到临时数组中,最后临时数组中的数据就是我们不需要删除的数据。

ArrayList 的批量删除的思想也是和这个类似的,所以我们在需要删除多个元素的时候,最好都使用这种批量删除的思想,而不是采用在 for 循环中使用单个删除的方法,单个删除的话,在每次删除的时候都会进行一次数组拷贝(删除最后一个元素时不会拷贝),很消耗性能,也耗时,会导致加锁时间太长,并发大的情况下,会造成大量请求在等待锁,这也会占用一定的内存。

public boolean removeAll(Collection<?> c) {
if (c == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 说明数组有值,数组无值直接返回 false
if (len != 0) {
// newlen 表示新数组的索引位置,新数组中存在不包含在 c 中的元素
int newlen = 0;
Object[] temp = new Object[len];
// 循环,把不包含在 c 里面的元素,放到新数组中
for (int i = 0; i < len; ++i) {
Object element = elements[i];
// 不包含在 c 中的元素,从 0 开始放到新数组中
if (!c.contains(element))
temp[newlen++] = element;
}
// 拷贝新数组,变相的删除了不包含在 c 中的元素
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
lock.unlock();
}
}

2.4查找

indexOf方法

// o:我们需要搜索的元素
// elements:我们搜索的目标数组
// index:搜索的开始位置
// fence:搜索的结束位置
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;
}

2.5迭代

// 1. 返回的迭代器是COWIterator
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}


// 2. 迭代器的成员属性
private final Object[] snapshot;
private int cursor;

// 3. 迭代器的构造方法
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}

// 4. 迭代器的方法...
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}

//.... 可以发现的是,迭代器所有的操作都基于snapshot数组,而snapshot是传递进来的array数组

CopyOnWriteArrayList在使用迭代器遍历的时候,操作的都是原数组!因此遍历时不用调用者显式加锁。

3.补充和小结:

3.1CopyOnWriteArrayList优点:

CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。

3.2CopyOnWriteArrayList缺点:

  • 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据,经常要执行​​add()、set()、remove()​​的话,那是比较耗费内存的。
  • 因为我们知道每次​​add()、set()、remove()​​这些增删改操作都要复制一个数组出来。
  • 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性
  • 从上面的例子也可以看出来,比如线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了(已经调用​​setArray()​​了)。但是线程A迭代出来的是原有的数据。

3.3CopyOnWriteSet

CopyOnWriteArraySet的原理就是CopyOnWriteArrayList。

private final CopyOnWriteArrayList<E> al;

public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}