CopyOnWriteArrayList源码分析

概述

Java自1.5后提供了两个写时复制的容器,分别是CopyOnWriteArrayList和CopyOnWriteArraySet。其思路就是在执行会改变底层数据的结构时,首先加锁,然后复制得到一个新的数据,在这个数据上做修改,最后再将原来的数据引用指向这个新的数据,最后释放锁;而读操作则不需要修改。这是一种读写分离的思想,读和写不同的容器,读的是旧容器,写的是新容器。
由于CopyOnWriteArrayList的实现原理和CopyOnWriteArraySet类似,所以就以CopyOnWriteArrayList抛个砖。
下图是CopyOnWriteArrayList的类继承关系,如下图:
CopyOnWriteArrayList源码分析_第1张图片
可以发现实现了List接口,直接继承了Object类,而没有像很多List一样继承AbstractList类。CopyOnWriteArrayList是一个自动扩容的List,允许任何元素,包括null。

源码分析

重要字段

CopyOnWriteArrayList名中含有“ArrayList”,所以其内部是基于数组实现的一个列表。为了使读写分离,所以不能在类自身上锁,内部含有一把锁,字段如下:

 /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

add(E e)方法

下面看一下添加一个元素的方法:

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        //加锁
        lock.lock();
        try {
            //得到旧数据
            Object[] elements = getArray();
            int len = elements.length;
            //创建新数据
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //操作新数据
            newElements[len] = e;
            //更改旧数据
            setArray(newElements);
            return true;
        } finally {
            //解锁
            lock.unlock();
        }
    }

从上面的代码可以看到添加一个元素add方法的执行流程:
1. 对Lock加锁
2. 一旦得到锁后,根据旧数据创建新数据
3. 更改新数据
4. 将引用指向新数据

上面代码中的setArray()方法就是将array引用指向新创建的数组,该方法的实现如下:

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

remove(int index)方法

下面再看一下rmeove(int index)方法,如果index超出了索引,那么会抛出IndexOutBoundsException,实现如下:

 public E remove(int index) {
        final ReentrantLock lock = this.lock;
        //加锁
        lock.lock();
        try {
            //得到旧数据
            Object[] elements = getArray();
            int len = elements.length;
            //获得元素,该方法可能会抛出IndexOutBoundsException
            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();
        }
    }

从上面的代码可以看到,remove()方法的执行流程和add()相同,都是:
1. 加锁
2. 复制旧数据,修改新数据
3. 将引用指向新数据
4. 释放锁

写方法总结

CopyOnWriteArrayList中的add、set、remove等方法会修改底层数组,所以当执行这些方法时会首先获取Lock锁,也就意味着同一时刻只能有一个线程在修改数据。

get(int index)方法

下面看一下get()方法的实现:

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

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

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

可以看到该方法没有加锁。

size()方法

下面是size()方法的实现:

public int size() {
        return getArray().length;
    }

读方法总结

CopyOnWriteArrayList中的get、size、isEmpty等方法不会修改底层数据,所以当执行这些方法时不会获取Lock锁,直接对旧数据进行读取。

拓展

Java中提供了CopyOnWriteArrayList和CopyOnWriteArraySet,而没有提供CopyOnWriteMap,但是经过上面的源码分析,我们已经知道了COW的原理,那就是写时加锁,读时不加锁。按照这种思想实现了一个CopyOnWriteMap,如下:

public class CopyOnWriteMap implements Map{
    private volatile Map internalMap;

    private ReentrantLock lock=new ReentrantLock();


    public CopyOnWriteMap() {
        internalMap = new HashMap();
    }

    //。。。带参数的构造方法,配置HashMap


    public V put(K key, V value) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Map newMap=new HashMap(internalMap);
            V v=newMap.put(key,value);
            internalMap=newMap;
            return v;
        } finally {
            lock.unlock();
        }
    }


    public V remove(Object key) {

        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Map newMap = new HashMap(internalMap);
            V val = newMap.remove(key);
            internalMap = newMap;
            return val;
        } finally {
            lock.unlock();
        }
    }


    public int size() {
        return internalMap.size();
    }


    public boolean isEmpty() {
        return internalMap.isEmpty();
    }


    public V get(Object key) {
        return internalMap.get(key);
    }

    ...其他方法
}

CopyOnWriteMap实现了Map接口,Map中的每个方法利用内部的HashMap实现,对于写操作对Lock加锁,而读操作则不加锁。

总结

使用场景

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。

缺点

CopyOnWrite容器有很多优点,不过也有两个缺点:
1. 内存占用问题: 由于每次写操作都会复制出一个对象出来,如果旧数据已经很大,那么复制的数据也会很大,可能会导致过多的GC
2. 数据一致性问题: COW容器只能确保最终数据的一致性,不能保证实时数据一致性。如果你希望写入的数据,马上能被读到,请不要使用COW容器。

原理

读写分离,写时加锁,读时不加锁。

你可能感兴趣的:(Java并发库源码解析)