01.JUC 集合 - CopyOnWriteArrayList

基本概念

CopyOnWriteArrayList 的实现原理实际与 ArrayList 大同小异。它们内部都通过维护一个数组来对数据进行操作。

不同的是它的处理写操作(包括 add、set、remove)是先将原始的数据通过Arrays.copyof 方法复制生成一份新的数组。然后在新的数据对象上进行写操作,写完后再将原来的引用指向到当前这个数据对象。这样保证了每次写都是在新的对象上。而 ArrayList 除非是内部数组需要扩容操作才会复制生成一份新的数组。

为了保证写操作的一致性,这里要对各种写操作要加一把重入锁(ReentrantLock)来控制。

内部构造

  • 成员变量,在 CopyOnWriteArrayList 中使用 array 表示它内部维护的数组 ,该数组用于存放数据。
private volatile transient Object[] array;

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

final Object[] getArray() {
    return array;
}
  • 构造函数,CopyOnWriteArrayList 在被构造时,默认会创建一个容量为 0 的数组。值得注意的是,数组容量是固定的,但是对该类的每个操作都会通过复制创建新的数组,并且能在复制过程完成数组的容量变更。
public CopyOnWriteArrayList() {
    // 关键 -> 为 0 是因为对数组的操作都会通过复制创建新的数组,一开始不用确定数组容量
    setArray(new Object[0]);
}

原理分析

1.添加操作

在 CopyOnWriteArrayList 类中,存在两种添加操作:指定位置、不指定位置。

添加操作使用该类内部的 ReentrantLock 来控制并发,该锁是独占锁,说明同一时间有且只有一个线程能对该类执行该操作。

单个元素的添加操作默认会创建(容量+1)的新数组,并且从旧数组复制所有元素。由于这个特性,导致了该类的写操作需要大面积复制数组,所以性能肯定很差

下面来看两种操作的实现过程:

  • 添加元素时不指定位置
// 内部维护一个重入锁。
transient final ReentrantLock lock = new ReentrantLock();

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

        // 添加到数组末尾;数组容量为 len+1 时,下标为 0 - len。
        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 {
        Object[] elements = getArray();

        // 校验指定位置
        int len = elements.length;
        if (index > len || index < 0){
            // 抛出异常...
        }

        Object[] newElements;

        // 需要移动的元素,为 0 表示直接放置在数组末尾
        int numMoved = len - index;
        if (numMoved == 0){
            newElements = Arrays.copyOf(elements, len + 1);
        }else {
            newElements = new Object[len + 1];

            // 复制 [0-index] 的元素到新数组的[0-index]
            System.arraycopy(elements, 0, newElements, 0, index);

            // 复制 [index-末尾]的元素到新数组的 [(index+1)-末尾]
            // 空出 index 位置用来添加新元素
            System.arraycopy(elements, index, newElements, index + 1, numMoved);
        }
        newElements[index] = element;
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}

2.查询操作

在 CopyOnWriteArrayList 类中,存在两种查询操作:查询指定位置的元素、查询指定的元素。
后者比前者多了一步查询元素位置的过程。

需要注意的是:在该类中只有查询操作不需要加锁。

下面来看实现过程:

  • 查询指定位置的元素
public E get(int index) {
    return get(getArray(), index);
}

@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}
  • 查询指定的元素
public boolean contains(Object o) {
    Object[] elements = getArray();
    return indexOf(o, elements, 0, elements.length) >= 0;
}

private static int indexOf(Object o, Object[] elements, int index, int fence) {
    // 区分是不是 null,该方法的原理与 Arraylist 一样。
    if (o == null) {
        for (int i = index; i < fence; i++)
            if (elements[i] == null){
                return i;
            }
    } else {
        for (int i = index; i < fence; i++){
            if (o.equals(elements[i])){
                return i;
            }
        }   
    }
    return -1;
}

3.修改操作

需要注意的是,虽然修改操作不改变数组的容量,但也会创建新的数组并从旧数组复制元素。

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

4.删除操作

该类的删除操作分为:删除指定位置的元素、删除指定元素,但是实现过程却大相径庭。下面来看实现过程:

  • 删除指定位置的元素
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 boolean remove(Object o) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;

        if (len != 0) {
            int newlen = len - 1;
            Object[] newElements = new Object[newlen];

            // ①比较 [0 - (newlen -1 = len -2)] 位置上的元素
            for (int i = 0; i < newlen; ++i) {
                // 比较 o1,o2
                if (eq(o, elements[i])) {
                    // 相等,则通过复制,将元素前移,覆盖掉 i 位置上的元素。
                    for (int k = i + 1; k < len; ++k){
                        newElements[k - 1] = elements[k];
                    }   
                    setArray(newElements);
                    return true;
                } else{
                    // 不相等执行复制操作
                    newElements[i] = elements[i];
                }
            }

            // ②比较 [newlen = len -1]位置上的元素,说明指定元素在数组末尾
            if (eq(o, elements[newlen])) {
                setArray(newElements);
                return true;
            }
        }
        return false;
    } finally {
        lock.unlock();
    }
}

// 比较 01,02 是否相等
private static boolean eq(Object o1, Object o2) {
    return (o1 == null ? o2 == null : o1.equals(o2));
}

总结

在 CopyOnWriteArrayList 中添加、修改、删除都属于写操作,查询属于读操作。关于读写问题,具有以下特性:

  • 写操作:需要大面积复制数组,而且需要锁保证数据的一致性,所以性能肯定很差。

  • 读操作:因为操作的对象和写操作不是同一个对象,而且读操作的线程之间也不需要加锁。

  • 读写同步:读和写之间的同步处理只是在写完后通过一个简单的 = 将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合在多线程里使用,绝对不会发生 ConcurrentModificationException

综上所述, CopyOnWriteArrayList 适合使用在读操作远远大于写操作的场景里,比如缓存。

    原文作者:JUC
    原文地址: https://blog.csdn.net/u012420654/article/details/56667899
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞