Java CopyOnWriteArrayList

408 阅读4分钟

从名字就可以看出来CopyOnWriteArrayList和ArrayList是有关联的。ArrayList是线程非安全的,多线程时官方推荐我们使用 "Collections.synchronizedList(new ArrayList(...))"或者自己加锁,其实Java还提供了一种线程安全的ArrayList,这个线程安全的List就是我今天所写的CopyOnWriteArrayList。

结构

CopyOnWriteArrayList底层和ArrayList一样都是数组。前者的数组被volatile关键字修饰,表示数组的"内存地址"一旦被修改,其他线程能立马感知到。当我们对它进行增删的操作时他会加锁,拷贝出新数组,在新数组上进行操作,更改好之后把新数组赋值给数组容器,最后解锁。注意,它只对写操作进行加锁,读没有加锁。它只是线程安全的,并不能一定得到实时的数据。

//锁
//性能上ReentrantLock和synchronized没有什么区别,
//但ReentrantLock相比synchronized而言功能更加丰富,
//使用起来更为灵活,也更适合复杂的并发场景
final transient ReentrantLock lock = new ReentrantLock();
//使用volatile修饰的数组容器
private transient volatile Object[] array;

类注释

1:ArrayList的线程安全变体。其中所有的可变操作(add,remove,set等)都是通过创建底层数组的新副本来实现的。
2:允许存储所有元素 ,包括null。
3:迭代过程中不会抛出ConcurrentModificationException异常。

方法

add,remove和set方法基本分四步走: 1:通过ReentrantLoct加锁,保证同一时刻数组只能被一个线程操作。 2:通过Arrays.copyOf拷贝出新数组。 3:在新数组上进行操作,并把新数组赋值给数组容器,保证数组的内存地址被修改。volatile监控的是内存的地址。 4:在finally里解锁,即使异常也能释放琐。

add(E)

//使用volatile修饰的数组容器
private transient volatile Object[] array;
// 从尾部添加元素
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 1:通过ReentrantLoct加锁,保证同一时刻数组只能被一个线程操作。
    lock.lock();
    try {
        //原数组
        Object[] elements = getArray();
        int len = elements.length;
        //2:通过Arrays.copyOf拷贝出新数组,新数组的长度是 + 1 的,因为新增会多一个元素
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //3:在新数组上进行操作,元素直接添加到数组的尾部
        newElements[len] = e;
        //并把新数组赋值给数组容器,保证数组的内存地址被修改,volatile监控的是内存的地址。
        setArray(newElements);
        return true;
    //4:在finally里解锁 ,即使异常也能释放琐。
    } finally {
        lock.unlock();
    }
}
final void setArray(Object[] a) {
    array = a;
}

remove(int)

// 删除指定下标位置的元素
public E remove(int index) {
    final ReentrantLock lock = this.lock;
    //1:通过ReentrantLoct加锁,保证同一时刻数组只能被一个线程操作。
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        //得到老值
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)//删除的数据是数组的尾部,拷贝范围0-(len-1)
           //2:通过Arrays.copyOf拷贝出新数组,
           //3:在新数组上进行操作
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            //2:通过Arrays.copyOf拷贝出新数组,
            //3:在新数组上进行操作
            // 删除的数据在数组的中间,分三步走
            // 1:设置新数组的长度减一,因为是减少一个元素
            // 2:从 0 拷贝到数组新位置
            // 3:从新位置拷贝到数组尾部
            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 {//4:在finally里解锁 ,即使异常也能释放琐。
        lock.unlock();
    }
}

set(int,E)

//替换指定下标位置的元素
public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    1:通过ReentrantLoct加锁,保证同一时刻数组只能被一个线程操作。
    lock.lock();
    try {
        Object[] elements = getArray();
        //得到旧元素
        E oldValue = get(elements, index);
        if (oldValue != element) {//旧值和新值不一样
            int len = elements.length;
            // 2:通过Arrays.copyOf拷贝出新数组。
            Object[] newElements = Arrays.copyOf(elements, len);
            //3:在新数组上进行操作,并把新数组赋值给数组容器,保证数组的内存地址被修改。volatile监控的是内存的地址。
            newElements[index] = element;
            setArray(newElements);
        } else {//旧值和新值一样
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    //4:在finally里解锁,即使异常也能释放琐。
    } finally {
        lock.unlock();
    }
}

迭代

迭代过程不会快速故障,是因为迭代过程中使用的是旧数组的引用。旧数组的结构不会发生变化。

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}
final void setArray(Object[] a) {
    array = a;//这里更改的只是array的指向的内存地址,并没有更改旧数组的结构。
}
static final class COWIterator<E> implements ListIterator<E> {
    ...
    private final Object[] snapshot;
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;//得到旧数组的引用
    }
    ...
}

CopyOnWriteArrayList对写加锁,读不加锁,我们加锁的时候可以借鉴一下。使用时我们更多的用于读操作多于写操作的场景。
CopyOnWriteArrayList只是保证线程安全,并不保证你得到或操作的数据是最新的。