CopyOnWriteArrayList介绍
150342129 魏亚楠
它相当于线程安全的*ArrayList。和ArrayList一样,它是个可变数组*;但是和ArrayList不同的时,它具有以下特性:
它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
它是线程安全的。
因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先将原有数组的元素拷贝到新数组中,然后在新的数组中做写操作,写完之后,再将原来的数组引用(volatile 修饰的数组引用)指向新数组。CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。
CopyOnWriteArrayList包含了成员lock。每一个CopyOnWriteArrayList都和一个互斥锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥访问。
CopyOnWriteArrayList包含了成员array数组,这说明CopyOnWriteArrayList本质上通过数组实现的。
java.util.concurrent包中定义常见集合类对应的并发集合类,用于高效处理并发场景,其中CopyOnWriteArrayList对应就是ArrayList。顾名思义CopyOnWrite,写时拷贝,这里写包括对集合类的修改操作,都会创建一个副本。
CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景
CopyOnWriteArrayList的基本思想是一旦对“容器”有修改就复制一份新的集合,在新的集合上进行修改,然后将新集合给旧的进行引用。这一部分需要加锁,,然而最大的好处是在“读”操作的时候不需要加锁。
CopyOnWriteArrayList的核心思想是利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的数组,在新的数组上面修改,然后将新数组赋值给旧数组的引用,并通过volatile 保证其可见性,通过Lock保证并发写。
CopyOnWriteArrayList的“动态数组”机制:
CopyOnWriteArrayList的“线程安全”机制:
CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其中所有可变操作(例如add,set等)都是通过对底层数组进行一次新的复制来实现的。这一般是需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量是,这种方法可能比其他替代方法更有效。在不能或不想进行同步遍历,但是又从并发线程中排除冲突时,是非常有用的。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出ConcurrentModificationException。创建迭代器以后,迭代器不会反映列表的添加,移除或者修改。在迭代器上进行的元素更改操作(例如remove,set和add)不受支持。这些方法将抛出UnsupportedOperationException。这个类比较简单,在一些可变操作下通过加锁并对底层数组进行一次复制来实现。
类的定义
public class CopyOnWriteArrayList
implements List, RandomAccess, Cloneable, java.io.Serializable
由此可以看出没有继承子类,实现接口和ArrayList一样。
关键属性
/** 锁保护所有的应用程序 */
transient final ReentrantLock lock = new ReentrantLock();
/** 数组使用private修饰限制访问,数组只能通过getarray/setarray访问,数组使用volatile修饰保证可见性,不读缓存直接读写内存使用transient修饰,表示序列化时忽略此字段(自己定制序列化操作) */
private volatile transient Object[] array;
CopyOnWriteArrayList构造函数
底层数据结构
//初始对象构造一个对象为0的list
public CopyOnWriteArrayList() {
//采用Object数组存储数据
setArray(new Object[0]);
}
final void setArray(Object[] a) {
array = a;
}
/**
保存列表的状态到一个流,也就是序列化
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
s.defaultWriteObject();
Object[] elements = getArray();
// 写出数组的长度
s.writeInt(elements.length);
//按顺序写出所有的元素
for (Object element : elements)
s.writeObject(element);
}
先调用s.defaultWriteObject()对非transient修饰的字段进行序列化操作
/**
反序列化
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
// 绑定到新锁
resetLock();
// Read in array length and allocate array
int len = s.readInt();
Object[] elements = new Object[len];
// 按照顺序读出所有的元素
for (int i = 0; i < len; i++)
elements[i] = s.readObject();
setArray(elements);
}
构造方法
public CopyOnWriteArrayList() {
setArray(new Object[0]); //默认创建一个空数组
}
public CopyOnWriteArrayList(Collection c) {
Object[] elements = c.toArray();
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);//拷贝一份数组
setArray(elements);
}
size方法,直接返回数组大小,说明array数组只包含实际大小的空间
public int size() {
return getArray().length;
}
get方法,和ArrayList中类似,不过没有index的范围判断,读操作是直接通过getArray方法获取Object数组,然后通过下标index直接访问数据。读操作并没有加锁,也没有并发的带来的问题,因为写操作是加锁写数组的副本,写操作成功将副本替换为原数据,这也是写时复制名字的由来
public E get(int index) {
return (E)(getArray()[index]);
}
private E get(Object[] a, int index) {
return (E) a[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) {
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();
}
}
set方法先通过lock加锁,然后获取index位置的旧数据,供最后方法返回使用
E oldValue = get(elements, index);
创建数组的副本,在副本上进行数据的替换
Object[] newElements = Arrays.copyOf(elements, len);
Arrays.copyOf(elements, len)方法将会从elements数组复制len个数据创建一个新的数组返回
然后在新数组上进行数据替换,然后将新数组设置为CopyOnWriteArrayList的底层数组
newElements[index] = element;
setArray(newElements);
最后在finally块里边释放锁
特定位置添加数据,添加数据和替换数据类似,先加锁,然后数组下标检查,接着创建数组副本,在副本里边添加数据,将副本设置为CopyOnWriteArrayList的底层数组
add方法,可以看到无论是在尾部还是指定位置添加,都有锁定和解锁操作,在设置值之前都先将原先数组拷贝一份并扩容至size+1大小。add()方法的实现很简单,通过加锁保证线程安全,通过Arrays.copyOf根据原数组复制一个新的数组,将要插入的元素插入到新的数组的对应位置,然后将新的数组赋值给array,通过volatile保证内存可见。
//在列表的指定位置插入指定的元素
//将当前处于该位置的元素移动和任何后续
//添加一个索引
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) // ==0表示插入的位置是数组的最后一个位置,所以该位置前面的元素原样不动复制到新的数组即可
// 这里通过复制elements数组生成一个新的数组,注意这里新的数组长度是原数组+1,所以新数组的最后一个元素是NULL
newElements = Arrays.copyOf(elements, len + 1);
else {
// 将原数组的0~index-1原样复制到新的数组中,
// 而index之后的元素对应复制到新数组的index+1之后,即中间空出一个位置用于放置带插入元素
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1, numMoved);
}
// 将element插入到新的数组
newElements[index] = element;
// 将更新底层数组的引用,由于array是volatile的,所以对其的修改能够立即被后续线程可见
setArray(newElements);
} finally {
// 释放锁
lock.unlock();
}
}
//结束指定的元素列表,把元素添加到指定列表
// 该方法相当于调用add(array.length, e)
//添加元素
public boolean add(E e) {
//获取该对象的锁
final ReentrantLock lock = this.lock;
// 获取“锁”,每次只有一个线程可进入临界区
lock.lock();
try {
// 获取原始”volatile数组“中的数据和数据长度。
Object[] elements = getArray();
int len = elements.length;
// 新建一个数组newElements,并将原始数据拷贝到newElements中;
// newElements数组的长度=“原始数组的长度”+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 将“新增加的元素”保存到newElements中。
newElements[len] = e;
// 将”volatile数组“引用指向newElements数组,这样旧数组就被GC回收了
setArray(newElements);
return true;
} finally {
// 释放“锁”
lock.unlock();
}
}
set方法,ArrayList中set方法直接改变数组中对应的引用,这里需要拷贝数组然后再设置。set()比add()更新简单,只需要复制一个新的数组,然后更新新的数组的指定位置的元素,然后更新引用即可。
//将此列表中的指定位置的元素用指定的元素替换
public E set(int index, E element) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
// 获取需更新的元素
Object oldValue = elements[index];
// //
// 需更新的值不等于原值(注意此处的不等是==,不是equals(),即oldValue和element必须是引用同一个对象才可)
if (oldValue != element) {
int len = elements.length;
// 复制一个新的数组,并将index更新成新的值,更新引用
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
// 此处由于更新的值与原值是同一个对象,所以其实可不更新引用
// 从注释可以看出更新的目的是出于写volatile变量
setArray(elements);
}
return (E) oldValue;
} finally {
// 释放锁
lock.unlock();
}
}
remove(int)方法,和指定位置添加类似,需要拷贝[0,index)和[index+1,len)之间的元素
//删除索引index处的元素
public E remove(int index) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object oldValue = elements[index];
int numMoved = len - index - 1;// 需要移动的元素的个数
if (numMoved == 0) // ==0表示删除的位置是数组的最后一个元素,只需要简单的复制原数组的len-1个元素到新数组即可
setArray(Arrays.copyOf(elements, len - 1));
else {
// 将原数组的0-index-1复制到新数组的对应位置
// 将原数组的index+1之后的元素复制到新数组,丢弃原数组的index位置的元素
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index, numMoved);
setArray(newElements);
}
return (E) oldValue;
} finally {
lock.unlock();
}
}
remove(Object)方法,分配一个len-1大小的新数组,遍历原来数组,如果找到则将原来数组以后的元素拷贝到新数组中并将list设置为新数组,否则直接给新数组赋值上原来数组。
public boolean remove(Object o) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (len != 0) {
// Copy while searching for element to remove
// This wins in the normal case of element being present
int newlen = len - 1;// 删除之后数组的长度
Object[] newElements = new Object[newlen];// 创建新的数组
for (int i = 0; i < newlen; ++i) {// 从0-len-1遍历原数组
if (eq(o, elements[i])) {// 如果是待删除元素,则将该元素之后的元素复制到新数组中
// found one; copy remaining and exit
for (int k = i + 1; k < len; ++k)
newElements[k - 1] = elements[k];
// 设置新数组
setArray(newElements);
return true;
} else
// 将该元素插入到新数组
newElements[i] = elements[i];
}
// 确认最后原数组一个元素是否与待删除元素相等,是的话直接将修改引用即可,因为前面已经为新数组赋完值了
// special handling for last cell
if (eq(o, elements[newlen])) {
setArray(newElements);
return true;
}
}
// 到这里说明数组中没有与待删除元素相等的元素,所以直接返回false,
// 但是这里并没有写volatile变量,看来set那里也只是写着好玩
return false;
} finally {
lock.unlock();
}
}
迭代器的实现
ArrayList中迭代器支持fastfail,一旦检测到遍历过程中发送了修改则会抛出ConcurrentModificationException;CopyOnWriteArrayList的迭代器由于修改的时候都会重新copy一份数组,因此不存在并发修改问题,也不会抛出ConcurrentModificationException。同样支持单向和双向迭代器,其iterator和listIterator方法都是通过内部类COWIterator创建,只是前者返回接口限定为单向迭代Iterator。
COWIterator定义:
/** 数组快照**/
private final Object[] snapshot;
/** 随后调用下一个元素返回元素的索引 */
private int cursor;
构造器
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
iterator和listIterator中会传递当前数组的引用和cursor(无参方法为0,有参数方法为对应值)
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}