聊聊并发:(十五)concurrent包并发容器之CopyOnWriteArrayList分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wtopps/article/details/85267115

前言

在前面的文章中,我们陆续介绍了concurrent包的各个类,包括几种锁的使用及其实现,并发辅助工具的使用及其实现,本篇开始,我们继续介绍concurrent包中的并发容器的使用及其实现机制。

本篇,我们先来看一下并发容器:CopyOnWriteArrayList。

CopyOnWriteArrayList介绍

CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。

其对并发场景下操作提供了更好的支持,其内部使用ReentrantLock进行并发的控制,它主要具有以下特性:

  • 1、所有元素都存储在数组里面, 只有当数组进行remove, update时才在方法上加上ReentrantLock, 拷贝一份snapshot的数组, 只改变snapshot中的元素,最后再赋值到CopyOnWriteArrayList中。
  • 2、所有的get方法只是获取数组对应下标上的元素(无需加锁控制)。

CopyOnWriteArrayList是典型的使用空间换时间的方式进行工作, 它主要适用于读多些少,并且数据内容变化比较少的场景(最好初始化时就进行加载数据到CopyOnWriteArrayList中)。

由于是采用“快照”的方式进行的存储,因此其内部数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出ConcurrentModificationException

CopyOnWriteArrayList的构造函数与方法列表,及其使用方法与ArrayList基本一致,在这里不再过多介绍。

CopyOnWriteArrayList源码分析

CopyOnWriteArrayList内部采用了一个数组存储数据,使用ReentrantLock进行并发控制,只有当数组进行remove、 update时才进行加锁操作,其实现了List、RandomAccess、Cloneable接口。

我们首先来看一下add方法的实现:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    //1、获取锁
    lock.lock();
    try {
        //2、获取当前数组元素
        Object[] elements = getArray();
        //3、获取数组长度
        int len = elements.length;
        //4、复制旧数组数据到新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //5、将新元素添加到新数组的尾部
        newElements[len] = e;
        //6、将新数组更新为当前数组
        setArray(newElements);
        return true;
    } finally {
        //7、释放锁
        lock.unlock();
    }
}

新增一个元素的操作比较简单,具体可以参考注释内容,可以发现其实它与ArrayList的add方法不同的是,CopyOnWriteArrayList会在开始操作前,上一把锁,进行并发的控制,然后再操作数据的时候,会创建一个副本,对副本进行操作。

这样做的好处是当使用迭代器进行迭代数组元素的时候,由于引用的对象是副本,因此不会抛出ConcurrentModificationException异常,但是由于每次新增操作都会创建一个新的数组,空间有一定的损耗,因此CopyOnWriteArrayList是比较适合读多写少的场景。

我们再来看一下remove方法,按下标删除元素的实现:

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    //1、获取锁
    lock.lock();
    try {
        //2、获取当前数组元素
        Object[] elements = getArray();
        //3、获取数组长度
        int len = elements.length;
        //4、获取要删除的下标的值
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        //5、如果数组长度减删除元素位置-1等于0,说明删除的元素的位置在len-1上,直接拷贝原数组的前len-1个元素
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            Object[] newElements = new Object[len - 1];
            //6、拷贝原数组0-index之间的元素(index 不拷贝)
            System.arraycopy(elements, 0, newElements, 0, index);
            //7、拷贝原数组index+1到末尾之间的元素 (index+1也进行拷贝)
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            //8、将新的数组设置为当前数组
            setArray(newElements);
        }
        //9、返回旧值
        return oldValue;
    } finally {
        //10、释放锁
        lock.unlock();
    }
}

remove方法的实现比较简单,具体可以参见代码注释,主要分为几个步骤:

  • 1、获取锁
  • 2、获取当前数组元素
  • 3、获取数组长度
  • 4、获取要删除的下标的值
  • 5、如果数组长度减删除元素位置-1等于0,说明删除的元素的位置在len-1上,直接拷贝原数组的前len-1个元素
  • 6、拷贝原数组0-index之间的元素(index 不拷贝)
  • 7、拷贝原数组index+1到末尾之间的元素 (index+1也进行拷贝)
  • 8、将新的数组设置为当前数组
  • 9、返回旧值
  • 10、释放锁

我们主要来看一下直接remove元素的方法的实现:

public boolean remove(Object o) {
    Object[] snapshot = getArray();
    //寻找要删除的元素的下标
    int index = indexOf(o, snapshot, 0, snapshot.length);
    //如果没找到,返回false,否则执行删除操作
    return (index < 0) ? false : remove(o, snapshot, index);
}

private static int indexOf(Object o, Object[] elements, int index, int fence) {
    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;
}

private boolean remove(Object o, Object[] snapshot, int index) {
    final ReentrantLock lock = this.lock;
    //1、获取锁
    lock.lock();
    try {
        //2、获取当前数组
        Object[] current = getArray();
        int len = current.length;
        //3、如果数组副本与当前副本不一致
        //这里findIndex的作用是,当执行break findIndex的时候,整个流程会退出,即if (snapshot != current)体中的逻辑会不再执行
        if (snapshot != current) findIndex: {
            //4、从index,len中取出一个较小的值prefix
            int prefix = Math.min(index, len);
            //5、从current数组中的prefix前个元素中寻找元素o,找到后,将其所在的位置,赋给index,然后break流程
            for (int i = 0; i < prefix; i++) {
                if (current[i] != snapshot[i] && eq(o, current[i])) {
                    index = i;
                    break findIndex;
                }
            }
            //6、如果index >= len,则说明元素o在另外的线程中已经被删除,直接return
            if (index >= len)
                return false;
            //7、如果current[index] == o,则代表,index位置上的元素o还在那边,break流程
            if (current[index] == o)
                break findIndex;
            //8、从current数组中再找一遍,如果在其他线程中被删除掉了,直接return false
            index = indexOf(o, current, index, len);
            if (index < 0)
                return false;
        }
        Object[] newElements = new Object[len - 1];
        //9、拷贝原数组0-index之间的元素(index 不拷贝)
        System.arraycopy(current, 0, newElements, 0, index);
        //10、拷贝原数组index+1到末尾之间的元素 (index+1也进行拷贝)
        System.arraycopy(current, index + 1,
                         newElements, index,
                         len - index - 1);
        //11、将新的数组设置为当前数组
        setArray(newElements);
        return true;
    } finally {
        //12、释放锁
        lock.unlock();
    }
}

上面的代码就是直接remove元素的实现,操作有点多,具体可以参见注释,其实可以看到,CopyOnWriteArrayList在remove元素时,进行了大量的线程之间的容错控制,防止多线程操作下出现问题。

总结

本篇我们介绍了CopyOnWriteArrayList的使用及其实现,其大体实现方式与ArrayList基本一致,对于部分方法提供了线程安全的支持。

我们来总结一下它的特性,它是使用了空间换时间的方式进行的实现,主要适用于读多些少,并且数据内容变化比较少的场景,例如白名单、黑名单等等。

下篇我们将会对ConcurrentHashMap的实现进行分析,敬请期待

更多Java干货文章请关注我的个人微信公众号:老宣与你聊Java

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/wtopps/article/details/85267115
今日推荐