并发List——CopyOnWriteArrayList
CopyOnWriteArrayList 是Java并发包中提供的一个线程安全的 ArrayList 实现。它的核心设计思想是写时复制。
其有几个核心的特点:
- 允许多个线程同时读,且不需要加锁
- 每次修改操作的时候,都会创建底层数组的一个新副本
- 迭代器基于创建时的数组快照,不会反应后续的修改
- 只有写操作才加锁,读操作不加锁
其核心数据如下:
//使用对象锁
final transient Object lock = new Object();
//底层数组
private transient volatile Object[] array;
CopyOnWriteArrayList源码分析
初始化源码
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
对于空参构造,是直接创建一个长度为0的数组。
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
如果构造的时候传入一个数组,将会复制一份数组作为底层数组。
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] es;
if (c.getClass() == CopyOnWriteArrayList.class)
es = ((CopyOnWriteArrayList<?>)c).getArray();
else {
es = c.toArray();
if (c.getClass() != java.util.ArrayList.class)
es = Arrays.copyOf(es, es.length, Object[].class);
}
setArray(es);
}
如果构造的时候传入一个集合,基本也是复制一份到本List
添加元素
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
可以看出,添加元素的时候,是直接加锁了的。 流程如下:
- 加锁,同一时刻只允许一个线程修改。
- 复制底层的数组,并且扩展一格,将要添加的元素加入
- 将新数组覆盖原先的数组
获取元素
public E get(int index) {
return elementAt(getArray(), index);
}
final Object[] getArray() {
return array;
}
static <E> E elementAt(Object[] a, int index) {
return (E) a[index];
}
当要获取指定位置的元素时,流程如下:
- 先获取底层数组
- 再通过下标访问
因为读的时候未加锁,因此会有弱一致性的问题:
- 当线程A已经获取了底层数组之后
- 线程B修改了数据,创建了一个新的数组覆盖原来的底层数组
- 线程A依然持有的是之前的底层数组,因此还是可以读到原来的数据
修改指定元素
public E set(int index, E element) {
synchronized (lock) {
Object[] es = getArray();
E oldValue = elementAt(es, index);
if (oldValue != element) {
es = es.clone();
es[index] = element;
}
// Ensure volatile write semantics even when oldvalue == element
setArray(es);
return oldValue;
}
}
流程如下:
-
加锁,确保只有一个线程能够修改底层数组
-
获取底层数组
-
判断修改值和原始值是否一致
- 如果一致则不需要复制数组
- 如果不一致则需要复制数组并修改对应的数据
-
设置回底层数组
删除数据
public E remove(int index) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
E oldValue = elementAt(es, index);
int numMoved = len - index - 1;
Object[] newElements;
if (numMoved == 0)
newElements = Arrays.copyOf(es, len - 1);
else {
newElements = new Object[len - 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index + 1, newElements, index,
numMoved);
}
setArray(newElements);
return oldValue;
}
}
流程如下:
-
加锁
-
判断删除的元素的位置
- 如果删除的元素在最末尾,直接复制原数组0~len-1
- 如果删除的元素在中间,分两段复制
-
最后重新设置为底层数组
弱一致性的迭代器
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
COWIterator(Object[] es, int initialCursor) {
cursor = initialCursor;
snapshot = es;
}
```
public boolean hasNext() {
return cursor < snapshot.length;
}
```
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
}
从上可以看出,构造的COWIterator是使用当前的底层数组快照来实现的,next()以及hasNext都是基于snapshot实现的。
因此在使用迭代器的过程中,就算有其他的线程修改了CopyOnWriteArrayList的底层数组,由于写时复制的关系,不会影响到snapshot。
下面给一个演示的示例:
public static void main(String[] args) throws IOException, InterruptedException {
CopyOnWriteArrayList<Integer> list=new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
Thread thread1=new Thread(()->{
Iterator<Integer> iterator = list.iterator();
try {
TimeUnit.SECONDS.sleep(2);
while(iterator.hasNext()){
System.out.print(iterator.next()+" ");
}
System.out.println();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread thread2=new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
list.remove(0);
list.remove(0);
list.remove(0);
list.remove(0);
list.add(5);
list.add(6);
list.add(7);
list.add(8);
});
thread1.start();
thread2.start();
thread1.join();
}
输出的结果依旧是: