并发集合类(1):CopyOnWriteArrayList

0 阅读3分钟

并发List——CopyOnWriteArrayList

CopyOnWriteArrayList​ 是Java并发包中提供的一个线程安全的 ArrayList 实现。它的核心设计思想是写时复制

其有几个核心的特点:

  1. 允许多个线程同时读,且不需要加锁
  2. 每次修改操作的时候,都会创建底层数组的一个新副本
  3. 迭代器基于创建时的数组快照,不会反应后续的修改
  4. 只有写操作才加锁,读操作不加锁

其核心数据如下:

//使用对象锁
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;
    }
}

可以看出,添加元素的时候,是直接加锁了的。 流程如下:

  1. 加锁,同一时刻只允许一个线程修改。
  2. 复制底层的数组,并且扩展一格,将要添加的元素加入
  3. 将新数组覆盖原先的数组

获取元素

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];
}

当要获取指定位置的元素时,流程如下:

  1. 先获取底层数组
  2. 再通过下标访问

因为读的时候未加锁,因此会有弱一致性的问题:

  1. 当线程A已经获取了底层数组之后
  2. 线程B修改了数据,创建了一个新的数组覆盖原来的底层数组
  3. 线程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;
    }
}

流程如下:

  1. 加锁,确保只有一个线程能够修改底层数组

  2. 获取底层数组

  3. 判断修改值和原始值是否一致

    • 如果一致则不需要复制数组
    • 如果不一致则需要复制数组并修改对应的数据
  4. 设置回底层数组

删除数据

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;
    }
}

流程如下:

  1. 加锁

  2. 判断删除的元素的位置

    • 如果删除的元素在最末尾,直接复制原数组0~len-1
    • 如果删除的元素在中间,分两段复制
  3. 最后重新设置为底层数组

弱一致性的迭代器

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();
}

输出的结果依旧是:

image.png