Java并发-CopyOnWriteArrayList类

CopyOnWriteArrayList类

java.util.concurrent并发包里的CopyOnWriteArrayList工具类。当有多个线程可能同时遍历、修改某个公共数组时候,如果不希望因使用synchronize关键字锁住整个数组而影响性能,可以考虑使用CopyOnWriteArrayList。

如果简单的使用读写锁的话,在写锁被获取之后,读写线程被阻塞,只有当写锁被释放后读线程才有机会获取到锁从而读到最新的数据,站在读线程的角度来看,即读线程任何时候都是获取到最新的数据,满足数据实时性。既然我们说到要进行优化,必然有 trade-off,我们就可以牺牲数据实时性满足数据的最终一致性即可。而 CopyOnWriteArrayList 就是通过 Copy-On-Write(COW),即写时复制的思想来通过延时更新的策略来实现数据的最终一致性,并且能够保证读线程间不阻塞。

COW 通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。对 CopyOnWrite 容器进行并发的读的时候,不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,延时更新的策略是通过在写的时候针对的是不同的数据容器来实现的,放弃数据实时性达到数据的最终一致性。

API

CopyOnWriteArrayList的定义如下:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

它也属于Java集合框架的一部分,是ArrayList的线程安全的变体,跟ArrayList的不同在于:CopyOnWriteArrayList针对数组的修改操作(add、set等)是基于内部拷贝的一份数据而进行的。换句话说,即使在一个线程进行遍历操作时有其他线程可能进行插入或删除操作,我们也可以“线程安全”得遍历CopyOnWriteArrayList。

  1. 创建一个CopyOnWriteArrayList数组numbers
CopyOnWriteArrayList<Integer> numbers = new CopyOnWriteArrayList<>(new Integer[]{1, 2, 8, 98});
  1. 创建一个遍历器iterator;
Iterator<Integer> iterator = numbers.iterator();
  1. 给numbers中增加(或删除、修改)一个元素
numbers.add(125);
  1. 利用iterator遍历数组的元素,发现遍历的结果是Iterator对象创建之前的
List<Integer> result = new LinkedList<>();
iterator.forEachRemaining(result::add);
assertThat(result).containsOnly(1, 3, 5, 78);

底层实现原理

底层是基于ReentrantLock 来实现线程安全的。
add()和remove()方法,是基于底层是基于ReentrantLock来加锁的。
get()是不加锁的,读到的是历史数据(副本)。

通过读写分离的思想,使得读读之间不会阻塞,无疑如果一个 list 能够做到被多个读线程读取的话,性能会大大提升不少。这是读写分离在数据结构上的应用和体现。

get()方法

public E get(int index) {
    return get(getArray(), index);
}
/**
 * Gets the array.  Non-private so as to also be accessible
 * from CopyOnWriteArraySet class.
 */
final Object[] getArray() {
    return array;
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

可以看出来 get 方法实现非常简单,几乎就是一个“单线程”程序,没有对多线程添加任何的线程安全控制,也没有加锁也没有 CAS 操作等等,原因是,所有的读线程只是会读取数据容器中的数据,并不会进行修改。

add方法

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    //1. 使用Lock,保证写线程在同一时刻只有一个
    lock.lock();
    try {
        //2. 获取旧数组引用
        Object[] elements = getArray();
        int len = elements.length;
        //3. 创建新的数组,并将旧数组的数据复制到新数组中
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //4. 往新数组中添加新的数据
        newElements[len] = e;
        //5. 将旧数组引用指向新的数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

优缺点

优点:读,速度快。解决了并发问题。
缺点:
1. 内存占有问题:很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以用ConcurrentHashMap来代替。
2. 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器

结论

CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。发生修改时候做copy,新老版本分离,保证读的高性能,适用于以读为主的情况。

应用场景

  • 比如可以缓存国家-城市-地区(县)
  • 比如系统配置的信息
    这些数据,因为这些数据很少改变,读操作远远大于写操作,是实现本地缓存的一个很好的数据结构。

(完)

发表评论

邮箱地址不会被公开。 必填项已用*标注