CopyOnWriteArrayList源码

一、简介

之前我们分析了ArrayList的源码,但它不是线程安全的,多线程环境下一些问题,比如并发导致数据丢失,并发导致插入null,并发导致数组越界等。所以今天我们来分析线程安全的CopyOnWriteArrayList,CopyOnWrite也就是写时复制。写时复制的思想是当我们往一个容器添加或者删除元素的时候,不直接往当前容器添加,而是将当前容器复制出一个新的容器,然后在新的容器里进行操作,操作完成之后,再将原来容器的引用指向新的容器。这样做的好处就是读写分离,读不加锁,写加锁,读写不冲突,提高并发性,比较适合读多写少的场景。

二、源码阅读

先来看看继承关系
CopyOnWriteArrayList源码_第1张图片
CopyOnWriteArrayList与ArrayList实现的接口是一样的,实现了List, RandomAccess, Cloneable, java.io.Serializable等接口。

2.1 属性

    /**
     * 对数组增删改时,用来加锁
     */
    final transient ReentrantLock lock = new ReentrantLock();
 
    /**
     * 存储元素的地方,使用volatile修饰,在写操作替换新数组完成之后,读操作能找到替换的新数组
     */
    private transient volatile Object[] array;

2.2 构造函数

public CopyOnWriteArrayList() {
    // 构建一个长度为0的数组,
    setArray(new Object[0]);
}

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

public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    // 如果c是CopyOnWriteArrayList类型
    if (c.getClass() == CopyOnWriteArrayList.class)
        // 那么直接使用c的数
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        // c不是CopyOnWriteArrayList类型,调用toArray()方法将集合元素转化为数组
        elements = c.toArray();
        // elements不是Object[]类型,转换为Object[]类型
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}

//把任意类型数组中的元素拷贝到Object[]类型的数组中。
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

2.3 add(E e)方法

添加一个元素到末尾

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 获取原来的数组
        Object[] elements = getArray();
        int len = elements.length;
        // 构建一个新数组,新数组大小是旧数组大小加1
        // 将旧数组元素拷贝到新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 将元素放在最后一位
        newElements[len] = e;
        //将array引用指向新的数组,原来的数组会被回收
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

add(int index, E element)方法
添加一个元素在指定索引处。

public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 获取原来的数组
        Object[] elements = getArray();
        int len = elements.length;
        // 检查是否越界
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        Object[] newElements;
        //计算需要移动的元素个数
        int numMoved = len - index;
        // numMoved为0,说明插入的位置是最后一位
        if (numMoved == 0)
            // 那么拷贝一个n+1的数组, 其前n个元素与旧数组一致
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            // numMoved不为0,说明插入的位置不是最后一位,那么新建一个n+1的数组
            newElements = new Object[len + 1];
            // 拷贝旧数组前index个元素到新数组中
            System.arraycopy(elements, 0, newElements, 0, index);
            // 将index及其之后的元素往后挪一位拷贝到新数组中,这样index的位置上是空的
            System.arraycopy(elements, index, newElements, index + 1, numMoved);
        }
        // 将元素放置在index处
        newElements[index] = element;
        setArray(newElements);
    } finally {
        // 释放锁
        lock.unlock();
    }
}

2.4 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);
        // 跟add方法一样,计算需要移动的元素个数
        int numMoved = len - index - 1;
        // numMoved为0,需要移除的是最后一位
        if (numMoved == 0)
            // 直接拷贝前面n-1元素形成的新数组, 最后一位就自动删除了
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            // 如果移除的不是最后一位,新建一个n-1的新数组
            Object[] newElements = new Object[len - 1];
            // 将前index的元素拷贝到新数组中
            System.arraycopy(elements, 0, newElements, 0, index);
            // 将index+1以及后的元素往前挪一位,这样原来index位置上的元素就被覆盖掉了, 相当于删除
            System.arraycopy(elements, index + 1, newElements, index, numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

2.5 set(int index, E element)方法
修改index索引上的元素

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    //加锁
    lock.lock();
    try {
        Object[] elements = getArray();
        // 获取index位置上原来的元素值
        E oldValue = get(elements, index);
        // 原来的元素的值不等于新值,则复制一份到新数组中再进行修改
        if (oldValue != element) {
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        } else {
            // 确保set的写语义
            setArray(elements);
        }
        return oldValue;
    } finally {
        //释放锁
        lock.unlock();
    }
}

2.6 get(int index)方法
获取index索引上的元素

public E get(int index) {
    // 获取元素不加锁,直接返回index位置的元素
    return get(getArray(), index);
}

final Object[] getArray() {
    return array;
}

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

因为get()方法不需要加锁,所以会出现这样一种情况,set()修改了数组中元素的值,但是还没有把将array引用指向新的数组,这时候get()方法会去原来的数组中获取,得到还没修改的值,这样就导致了数据的不一致,如果在set()方法将array引用指向新的数组之后,再去调用get()去获取值,就能得到修改后的值 ,所以说CopyOnWriteArrayList不是数据强一致性的,而是最终一致性的。

三、总结

(1) CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作使得占用内存翻倍,所以适用于读多写少的场合;

(2)CopyOnWriteArrayList的写操作,先获取到ReentrantLock锁,保证线程安全,然后拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下;

(3)CopyOnWriteArrayList的读操作不加锁,可能获取不到最新修改的元素,只保证最终一致性,不保证实时一致性。

你可能感兴趣的:(java并发,java集合)