CopyOnWriteArrayList源码分析
作用
CopyOnWriteArrayList(Copy-On-Write: 写入时复制)是一个线程安全的ArrayList,对其进行的修改操作都是先加锁然后在底层的一个复制数组上进行。
优点: 经常被用于"读多写少"的并发场景,因为读取的时候不需要加锁,性能较好。读写分离,在使用迭代器迭代的时候不会抛出异常
缺点: 需要拷贝原数据,数据较大的时候容易引起频繁Full GC;写和读在不同的数组上,读取的是老数组的数据(弱一致性问题)
成员变量
// 独占锁
final transient ReentrantLock lock = new ReetrantLock();
// 只能通过getArray/setArray方法来获取/修改array
private transient volatile Object[] array;
初始化
1. 创建空链表
public CopyOnWriteArrayList() {
setArrayList(new Object[0]);
}
2. 根据给定的集合数据创建链表
public CopyOnWriteArrayList(Collections<? extends E> c) {
Object[] elements;
// 如果是CopyOnWriteArrayList类,直接引用
if (c.getClass() == CopyOnWriteArrayList.class) {
elements = ((CopyOnWriteArrayList<?>)c).getArray();
} else {
elements = c.toArray();
// c.toArray可能不会返回Object[]类型
if (elements.getClass() != Object[].class) {
// 通过Arrays.copyOf复制数组
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
}
// 设置array变量为elements
setArray(elements);
}
3. 根据数组创建链表
public CopyOnWriteArrayList(E[] toCopyIn) {
// 上面方法的简化
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
获取元素
get(int index)
获取array[index]位置的值
**注意: **这里假设一个情景:将get分为两步: 1. 获取array、2. 读取指定下标的值。在线程x执行完第一步还未执行第二步时,线程y对链表进行了修改,那么get方法返回的还是未修改数组里对应的值。
因为对数组的修改操作都是需要加锁之后对数组进行拷贝,对拷贝数组操作后再修改array指向拷贝数组,但是此时线程x已经将getArray()获取到的数组(原数组)压入方法栈中,因此使用的是原数组的引用,即时线程y将array指向了拷贝数组也不影响原数组
public E get(int index) {
return get(getArray(), index);
}
修改
set(int index, E element)
修改array[index]的值
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
// 拿到成员变量array
Object[] elements = getArray();
// 获取elements[index]的值
E oldValue = get(elements, index);
// 如果值发生了变化
if (oldValue != element) {
int len = elements.length;
// 先对elements复制一个拷贝
Object[] newElements = Arrays.copyOf(elements, len);
// 更新值
newElements[index] = element;
// 更新this.array = newElements
setArray(newElements);
} else {
// 为了保证volatile语义
setArray(elements);
}
// 返回原始值
return oldValue;
} finally {
// 释放锁
lock.unlock();
}
}
添加
add(E e)
添加元素到末尾
// 和上面的set差不多 流程: 先获取锁->拷贝数组->修改拷贝数组->修改原始数组引用
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 拷贝数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 添加值
newElements[len] = e;
// 修改array引用
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}
删除
remove(int index)
删除index位置的值
public E remove(int index) {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// index位置的值
E oldValue = get(elements, index);
// index之后需要移动的元素个数
int numMoved = len - index - 1;
// 不需要移动
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
// 创建新数组
Object[] newElements = new Object[len - 1];
// 拷贝删除位置前面的元素
System.arraycopy(elements, 0, newElements, 0, index);
// 拷贝删除元素后面的元素
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
// 修改array引用
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
remove(Object o, Object[] snapshot, int 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();
}
}
迭代器
迭代器和get读取一样,如果读的过程中其他线程对数据进行了修改,仍然读取老数据的值
public Iterator<E> iterator() {
// 返回的是COWIterator的实例
return new COWIterator<E>(getArray(), 0);
}
接下来看COWIterator的具体实现
static final class COWIterator<E> implements ListIterator<E> {
// 数组的拷贝
private final Object[] snapshot;
// 当前下标
private int cursor;
// 这里可以参考get(int index)方法
// 在调用完getArray()方法后已经将原数组的引用压入到当前线程的栈中,其他线程是将array指向其他数组,对原始数组并没修改
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();
}
}
使用场景
假如我们有一个搜索的网站需要屏蔽一些“关键字”,“黑名单”每晚凌晨定时更新,每当用户搜索的时候,“黑名单”中的关键字不会出现在搜索结果当中,并且提示用户敏感字。
这里因为凌晨接口被访问的可能性低,CopyOnWriteArrayList只能保证数据最终一致,只要在修改数据的时候没有被读取即可
参考
- 深入浅出Java多线程 16 CopyOnWriteList
- 《Java并发编程之美》