Java并发编程(十八)Java 并发包中并发List源码剖析

逆流者 2021年02月21日 77次浏览

此文为读书笔记,欢迎评论,讨论问题,共同进步!


介绍

并发包中的并发List只有CopyOnWriteArrayListCopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略。类图结构如图:
在这里插入图片描述

在 CopyOnWriteArrayList 的类图中,每个CopyOnWriteArrayList对象里面有一个 array数组对象用来存放具体元素,ReentrantLock独占锁对象用来保证同时只有一个线程对 array 进行修改。这里只要记得 ReentrantLock是独占锁,同时只有一个线程可以获取。

设计一个线程安全的 list 所要考虑的问题:

  1. 何时初始化 list,初始化的 list 元素个数为多少,list 是有限大小吗?
  2. 如何保证线程安全,比如多个线程进行读写时如何保证是线程安全的?
  3. 如何保证使用迭代器遍历 list 时的数据一致性?

带着上面的问题往下看。

主要方法源码解析

初始化

// 无参构造函数,内部创建了一个大小为0的Object数组作为 array 的初始值
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
// 入参为集合,将集合里面的元素复制到本list
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}
// 创建 list ,其内部元素是入参toCopyin 的副本
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

final void setArray(Object[] a) {
    array = a;
}

private transient volatile Object[] array;

添加元素

  • boolean add(E e)
  • void add(int index, E element)

一共两个添加方法,源码也比较简单:

public boolean add(E e) {
	// 获取独占锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    	// 获取 内部 array
        Object[] elements = getArray();
        int len = elements.length;
        // 复制 array到新数组 添加元素到新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        // 用新数组替换旧的数组
        setArray(newElements);
        return true;
    } finally {
    	// 释放独占锁
        lock.unlock();
    }
}

public void add(int index, E element) {
	// 获取独占锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    	// 获取 内部 array
        Object[] elements = getArray();
        int len = elements.length;
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        // 新数组
        Object[] newElements;
        // 如果当前index等于源数组的长度,说明index下标添加到源数组的尾部
        int numMoved = len - index;
        if (numMoved == 0)
        	// 和boolean add(E e)一致,当前元素要添加到尾部
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            newElements = new Object[len + 1];
            // System.arraycopy(源数组, 源数组起始位置, 目标数组, 在目标数据中的起始位置, 要复制的数组元素的数量);
            // 从原数组中复制下标0到index-1的数据到新数组
            System.arraycopy(elements, 0, newElements, 0, index);
            // 从原数组中复制下标index到(len - index)就是到最后 的数据到新数组
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        // 新数组中空出的index位置的数据添加数据
        newElements[index] = element;
        // 用新数组替换旧的数组
        setArray(newElements);
    } finally {
    	// 释放独占锁
        lock.unlock();
    }
}

获取指定位置元素

public E get(int index) {
   return get(getArray(), index);
}
final Object[] getArray() {
    return array;
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

上面源码很简单,可以看出获取指定位置的元素分两步:

  • 第一步:获取内部数组array
  • 第二步:获取数组中指定位置的元素

上面代码中并没加锁,会有写时复制策略产生的弱一致性问题:
比如数组中有1,2,3 元素,A线程get(0)只执行到第一步,这是B线程remove(0)了,这时A线程执行完了第二步,返回的还是1。

修改指定元素

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 {
            // 不是完全禁止操作;确保可变的写语义
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

如上代码首先获取了独占锁,从而阻止其他线程对 aray数组进行修改,然后获取当前数组,并调用 get方法获取指定位置的元素,如果指定位置的元素值与新值不一致则创建新数组并复制元素,然后在新数组上修改指定位置的元素值并设置新数组到aray。如果指定位置的元素值与新值一样,则为了保证volatile语义,还是需要重新设置aray,虽然 array 的内容并没有改变。

删除元素

删除list里面指定的元素有三个方法:

  • E remove(int index)
  • boolean remove(Object o)
  • boolean remove(Object o, Object[] snapshot, int index)

下面说下E remove(int index)方法,其他两个类似,不做赘述。

public E remove(int index) {
	// 获取独占锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    	// 获取数组
        Object[] elements = getArray();
        int len = elements.length;
        // 获取指定元素
        E oldValue = get(elements, 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);
            // 使用新数组替换老数组
            setArray(newElements);
        }
        return oldValue;
    } finally {
    	// 释放锁
        lock.unlock();
    }
}

弱一致性的迭代器

遍历列表元素可以使用迭代器。先看一个例子:如何使用迭代器。

public static void main(String[] args) throws InterruptedException {
    CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList();
    arrayList.add("hello");
    arrayList.add("world");

    Iterator<String> iterator = arrayList.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}
hello
world

迭代器的hasNext方法用于判断列表中是否还有元素,next方法则具体返回元素。

下面来看 CopyOnWriteArrayList 中迭代器的弱一致性是怎么回事,所谓弱一致性是指返回迭代器后,其他线程对 list 的增删改 对迭代器是不可见的。

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

static final class COWIterator<E> implements ListIterator<E> {
    /** array 的快照版本 */
    private final Object[] snapshot;
    /** 数组下标  */
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

	// 是否遍历结束
    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    ...

	// 获取元素
    @SuppressWarnings("unchecked")
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    ...
}

在如上代码中,当调用iterator()方法获取迭代器时实际上会返回一个COWIterator对象,COWIterator对象的snapshot变量保存了当前list的内容,cursor是遍历list时数据的下标。

为什么说snapshot是 list 的快照呢?明明是指针传递的引用啊,而不是副本。如果在该线程使用返回的迭代器遍历元素的过程中,其他线程没有对list进行增删改,那么 snapshot本身就是 list 的 array,因为它们是引用关系。但是如果在遍历期间其他线程对该 list 进行了增删改,那么snapshot就是快照了,因为增删改后list里面的数组被新数组替换了,这时候老数组被snapshot引用。这也说明获取迭代器后,使用该迭代器元素时,其他线程对该 list 进行的增删改不可见,因为它们操作的是两个不同的数组,这就是弱一致性。

下面通过一个例子来演示多线程下迭代器的弱一致性的效果。

public class CopyListTest {

    private static volatile CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        arrayList.add("I ");
        arrayList.add("love ");
        arrayList.add("you ");
        arrayList.add("china!");

        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                arrayList.set(0, "We ");
                arrayList.remove(3);
            }
        });

        // 在线程启动之前获取迭代器
        Iterator<String> iterator = arrayList.iterator();

        // 启动线程
        threadA.start();

        // 等子线程执行结束
        threadA.join();

        // 迭代元素
        while (iterator.hasNext()) {
            System.out.print(iterator.next());
        }
    }
}

I love you china!

从输出结果我们知道,在子线程里面进行的操作一个都没有生效,这就是迭代器弱一致性的体现。需要注意的是,获取迭代器的操作必须在子线程操作之前进行。

总结

CopyOnWriteArrayList 使用写时复制的策略来保证 list 的一致性,而获取一修改—写入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有一个线程能对 list 数组进行修改。另外 CopyOnWriteArayList 提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list的修改是不可见的,迭代器遍历的数组是一个快照。另外,CopyOnWriteArraySet 的底层就是使用它实现的。