【Java】集合类 - List - CopyOnWriteArrayList

Posted by 西维蜀黍 on 2019-03-18, Last Modified on 2023-02-28

背景

CopyOnWriteArrayList容器是Collections.synchronizedList(List list)的替代方案。

CopyOnWriteArrayList在某些情况下具有更好的性能(读操作远多于写操作时)。在传统的使用 synchronized 关键字以保证互斥操作的实现中(比如 Vector),通常会把读操作进行加锁,因而任一时刻只能有一个读线程能够获得锁,所以其他的读线程都必须等待,大大影响性能。

CopyOnWriteArrayList称为“写时复制”容器,就是在多线程对容器对象进行读写操作时,执行写操作的线程在写的同时,还会把容器复制一份(并对复制后的容器实例进行写修改操作),最终就可以做到写操作进行的同时,不阻塞其他线程的读操作。

从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。

CopyOnWriteArrayList

CopyOnWriteArrayList实现

先说说CopyOnWriteArrayList容器的实现原理。

简单地说,就是在需要对容器进行操作的时候,将容器拷贝一份,对容器的修改操作都在容器的拷贝副本中进行。当修改操作结束,再用容器的拷贝副本替换掉原来的容器。

这样设计的好处是实现了读写分离,并且读读时不会发生阻塞。

下面的源码是CopyOnWriteArrayList的add方法实现:

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;
            //1、复制出一个新的数组
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            //2、把新元素添加到新数组中
            newElements[index] = element;
            //3、把数组指向原来的数组
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }

上面的三个步骤实现了写时复制的思想,在读数据的时候不会锁住list,因为写操作是在原来容器的拷贝上进行的。而且,可以看到,如果对容器拷贝修改的过程中又有新的读线程进来,那么读到的还是旧的数据。读的代码如下:

public E get(int index) {
        return get(getArray(), index);
    }

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

适用场景

CopyOnWriteArrayList 允许在写操作的同时进行读操作,因而大大提高了读操作的性能,因此很适合读多写少的应用场景,比如白名单,黑名单,商品类目的访问和更新场景,而不适合内存敏感以及对实时性要求很高的场景。

CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

优缺点分析

了解了CopyOnWriteArrayList的实现原理,分析它的优缺点及使用场景就很容易了。

优点:

读操作性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。Java的list在遍历时,若中途有别的线程对list容器进行修改,则会抛出ConcurrentModificationException异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了

缺点:

缺点也很明显:

  • 一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份,数据量大时,对内存压力较大,可能会引起频繁GC;
  • 二是无法保证数据不一致,Vector对于读写操作均加锁同步,可以保证读和写的强一致性。而CopyOnWriteArrayList由于其实现策略的原因,写和读分别作用在新老不同容器上,在写操作执行过程中,读虽然不会阻塞,但读取到的却是老容器的数据(或者说是旧数据)。

CopyOnWriteArraySet

原理:基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent方法,其遍历当前Object数组,如Object数组中已有了当前元素,则直接返回,如果没有则放入Object数组的尾部,并返回。

Reference