容器一般分为三类:普通容器,同步容器,并发容器。
对于容器我们每天都再用,无非就是对容器的增删改查和迭代,单个操作都没有问题,加上多线程有修改、有迭代、有查询。如果你遇到如下问题但对原理说不清楚,还是建议先看下我之前的博文Java容器迭代时修改问题及方案
问题诸如:
迭代时没有则添加抛出异常ConcurrentModificationException
迭代时有则删除,但是删除不干净 (比如两个紧挨着的相同的对象只能删除一个)
再复合操作中,用了同步容器(线程安全的)如Vector,仍然抛出异常ConcurrentModificationException
今天讲这个COW(写时复制)的list,在熟悉Java锁ReentrantLock源码的情况下在读本文就是轻车熟路,非常简单。
想到CopyOnWriteArrayList脑子中应该立马出现几个关键词:
1)线程安全的List
2)脏读
3)写时复制COW
4)底层volatile Object[] 初始0长度
5)读多写少场景
就说明你理解的差不多了,我们还是过下源码吧!
重要属性
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
一把独占重入锁
一个数组Object[] volatile修饰
构造方法
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
没有默认容量,ArrayList默认是10,不像ArrayBlockingQueue需要设置容量
增:add(E e)方法
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;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
add方法是互斥的,没有null检查,不像ArrayBlockingQueue非null
1)lock.lokc()我们熟悉的锁
2)拷贝数组增加一个容量
3)给数组最后一个位置复制为当前添加的对象
4)给数组引用复制(volatile修饰保证写能被其它线程可见)
不考虑独占重入锁锁,这段逻辑变得异常的简单。
问:为什么不像ArrayList一样能够扩容,而是每次添加都会拷贝数组?
答:COW是写时复制 适合读多写少场景,所以写场景少,每次加1个容量,不会浪费空间。
查:indexOf(obj)方法
public int indexOf(Object o) {
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length);
}
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
返回找到第一个
符合条件的对象的下标
为空使用elements[i] == null判断
不为空obj.equals(elements[i])
找不到返回-1
读取到的是可能脏数据。
删:remove(obj)方法
外层remove方法
public boolean remove(Object o) {
Object[] snapshot = getArray();
int index = indexOf(o, snapshot, 0, snapshot.length);
return (index < 0) ? false : remove(o, snapshot, index);
}
remove调用的内层的内部支撑方法remove(o,snapshot,index)
remove(obj)内部调用了重载的私有支撑方法remove(obj,snapshot,index)
在外层没有锁的情况下通过equals来比较找到下标,前面说过这个下标的值有可能已经被修改成了其它,所以真正执行remove的时候还是要加锁snapshot!=current就说明是—此时就是可能
脏读,如果该另外的线程正好改掉的就是这个下标的值才是真的
脏读。所以需要遍历重新判断,如果没有被修改再删除这个值,再次执行数组拷贝,再次赋值。
addIfAbsent(E e)
我们知道List可以放入重复元素,Set放入的是不可重复的,我们看下CopyArrayList是如何执行的。
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
代码依然是比较简单
先不加锁查询没有,
再执行真正的addIfAbsent(e,snapshot),
真正执行的时候加锁判断是否被别人修改过,
修改过(snapshot != current)并且current[i] != snapshot[i] && eq(e, current[i])即两个元素下标不相同,但是元素是相同的,说明已经被别人线程添加过了直接返回false,来保证没有重复的元素。
迭代器Iterator
我们知道所有的集合都有Iterator
static final class COWIterator implements ListIterator {
/** 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;
}
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;
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code remove}
* is not supported by this iterator.
*/
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code set}
* is not supported by this iterator.
*/
public void set(E e) {
throw new UnsupportedOperationException();
}
/**
* Not supported. Always throws UnsupportedOperationException.
* @throws UnsupportedOperationException always; {@code add}
* is not supported by this iterator.
*/
public void add(E e) {
throw new UnsupportedOperationException();
}
@Override
public void forEachRemaining(Consumer super E> action) {
Objects.requireNonNull(action);
Object[] elements = snapshot;
final int size = elements.length;
for (int i = cursor; i < size; i++) {
@SuppressWarnings("unchecked") E e = (E) elements[i];
action.accept(e);
}
cursor = size;
}
}
一点也不意外,也是找了以前的老的引用–snapshot,所有的迭代都是在迭代老的数组,同时迭代器不支持写操作,add,set,remove会抛出异常。
所有的size,contains也都是统计的老的数组的值。toArray也是拷贝。
总结:
COW写时复制,适合读多写少场景
读是可以并发的,读的数据可能是老的
写时互斥的,互斥还是通过ReentrantLock来控制的。
底层是没有容量的volatile修饰的Object[]
锁是整个并发包的基础,读锁事半功倍。