Java系列-CopyOnWriteArrayList源码解析

CopyOnWriteArrayList 是 ArrayList 的线程安全版本。用一句话归纳它的特色就是:全部的修改操做都是基于副原本进行的。java

设计思想

java.util.concurrent 下,有不少线程安全的容器,大体能够分红三类 Concurrent*CopyOnWrite*Blocking*这三类的容器均可以在并发环境下使用,可是实现的方式却不同。数组

Concurrent* 容器是基于无锁技术实现,性能很好,ConcurrentHashMap 就是典型表明;CopyOnWrie* 容器则是基于拷贝来实现的,因此对于内存有很大的开销,CopyOnWriteArrayList 就属于这一类;Blocking* 容器则使用 锁技术实现了阻塞技术,在某些场景下很是有用。安全

CopyOnWriteArrayList 的核心操做以下,就是经过不断的拷贝数组来更新容器:微信

具体实现

CopyOnWriteArrayList 的成员变量以下:多线程

final transient Object lock = new Object();
private transient volatile Object[] array;
复制代码

变量的数量不多,仅仅包含一个锁对象和一个用来放元素数组。由于 CopyOnWriteArrayList 保证线程安全的方式很简单,不断的经过备份元素来保证数据不会被修改。并发

如何实现线程安全

和其余线程安全的容器思路不同,这个容器从空间的角度来解决线程安全的问题。全部对容器的修改是基于副本进行的,修改的过程当中也经过锁对象锁来保证并发安全,从这个角度来讲,CopyOnWriteArrayList 的并发度也不会过高。因此一句话归纳就是使用 synchronized + Array.copyOf 来实现线程安全。函数

迭代器是基于副本进行的,即便原数组被改变,副本也不会被影响。也就不会抛出 ConcurrentModificationException 异常。可是这样也会让最新的修改没法及时体现出来。源码分析

核心方法的实现

get 方法直接读取数组就行,不须要上锁,多个线程同时读也就不会有并发的问题产生。post

public E get(int index) {
    return elementAt(getArray(), index);
}
static <E> E elementAt(Object[] a, int index) {
    return (E) a[index];
}
复制代码

下来来看一下 add 方法,代码很短:性能

// CopyOnWriteArrayList.add()
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,而后把元素添加进数组,最后用新数组去替代旧数组,完成添加。这样 CopyOnWriteArrayList 根本就不须要扩容,由于每次添加元素都是一个扩容的过程。

// CopyOnWriteArrayList.remove()
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,这就至关于缩容过程。

CopyOnWriteArrayList 的迭代器的实现也很不复杂:

# COWIterator 构造函数
COWIterator(Object[] es, int initialCursor) {
    cursor = initialCursor;
    // 容器元素的副本
    snapshot = es;
}
复制代码

能够看到,构造迭代器的时候,直接把整个元素的副本都传进来了,后续的操做都会在这个副本上进行,甚至都须要上锁。因此是 fail-safe 的。

在 CopyOnWriteArrayList 中,有两种数组拷贝方式 Arrays.copyOfSystem.arraycopy。这两种方式有什么区别吗?其实是没有的,来看一下 Arrays.copyOf 的源码:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                        Math.min(original.length, newLength));
    return copy;
}
复制代码

没错,Arrays.copyOf 调用了 System.arraycopy 来实现数组拷贝。

经过上面的分析可知,CopyOnWriteArrayList 的读效率很高,可是写的效率很低,因此比较适合读多写少的场景。

另外须要说一句,CopyOnWriteArraySet 使用 CopyOnWriteArrayList 实现。Set 一如继往喜欢使用现成的类来实现。

原文

相关文章

关注微信公众号,聊点其余的

相关文章
相关标签/搜索