Android CopyOnWriteArrayList 分析

说明

CopyOnWrite容器我们可以理解为写的时候复制的容器,最简单的理解就是当我们往里面添加元素的时候,不直接往当前的容易添加,而是先将当前容易拷贝一份,复制成一个新的容器,然后在新的容器里添加元素,添加完元素以后,再将原容器的引用指向新的容器。这样子我们就非常轻松的实现了读写分离的操作。从单词的后半部分来看其内部存储跟ArrayList都是使用了数组进行数据存储的,而且添加、修改、删除、查询数据的方法名字都是一样的。

类结构图

image_1e9kh210r1vvf1b8s18nn1e1r181b9.png-24kB

源码分析

  • 主要属性
//这个变量主要是用于同步代码加锁使用 synchronized (lock) 使用
final transient Object lock = new Object();
//该变量主要用于指向 实际存储数据元素的 数组
private transient volatile Object[] array;
  • 构造方法
//默认构造函数,同时创建一个空数组,同时指向属性 array 变量
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    //首先判断 c为 CopyOnWriteArrayList 类型,直接通过 getArray获取数组
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        elements = c.toArray();
        //拷贝数组元素
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}

public CopyOnWriteArrayList(E[] toCopyIn) {
    //拷贝数组元素
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
  • 添加元素

最后位置添加元素

public boolean add(E e) {
    //首先对代码块进行加锁,防止其他的线程修改 array数组
    synchronized (lock) {
        //首先获取旧数组  数组
        Object[] elements = getArray();
        int len = elements.length;
        //创建一个新数组长度为 旧数组长度 + 1,同时将旧数据拷贝到新数组中。
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //将要添加的数据赋值到新数组的末尾
        newElements[len] = e;
        //同时修改 array指向为新数组
        setArray(newElements);
        return true;
    }
}

指定位置添加元素

public void add(int index, E element) {
    //首先对代码进行加锁
    synchronized (lock) {
        Object[] elements = getArray();
        int len = elements.length;
        //判断位置是否在数组中是否越界
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException(outOfBounds(index, len));
        Object[] newElements;
        int numMoved = len - index;
        //如果插入的位置是最后一个位置,则直接拷贝旧数组所有数据拷贝新数组中
        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);
        }
        newElements[index] = element;
        setArray(newElements);
    }
}
  • 删除元素

删除指定位置元素

public E remove(int index) {
    //首先加锁
    synchronized (lock) {
        Object[] elements = getArray();
        int len = elements.length;
        //获取旧数组中指定位置的数据
        E oldValue = get(elements, index);
        //计算后面需要移动元素的个数
        int numMoved = len - index - 1;
        //如果删除的元素的位置是最后一个位置的话,则将 len以前的数据拷贝到新数组中
        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);
            //再将新数组赋值给 array 成员变量
            setArray(newElements);
        }
        //返回删除的数据
        return oldValue;
    }
}

删除具体元素

public boolean remove(Object o) {
    Object[] snapshot = getArray();
    //首先遍历数组,找到该元素所在的位置。index 小于0表示没有找到该元素,直接返回false
    int index = indexOf(o, snapshot, 0, snapshot.length);
    return (index < 0) ? false : remove(o, snapshot, index);
}
  • 修改元素

指定位置修改元素

public E set(int index, E element) {
    synchronized (lock) {
        Object[] elements = getArray();
        //首先获取旧数组中该位置的数据
        E oldValue = get(elements, index);
        //修改的新数据与之前的旧数据一致不一样
        if (oldValue != element) {
            int len = elements.length;
            //拷贝一份新的数组数据
            Object[] newElements = Arrays.copyOf(elements, len);
            //直接 index 位置数据
            newElements[index] = element;
            setArray(newElements);
        } else {
            setArray(elements);
        }
        //返回修改前的旧数据
        return oldValue;
    }
}
  • 查询元素

获取指定位置元素,支持随机访问。时间复杂度为 O(1)

public E get(int index) {
    //获取元素不加锁,直接返回index位置的元素。
    return get(getArray(), index);
}

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

疑惑

根据上面的代码我们知道改变数组中的数据时都会进行加锁操作的,比如说添加、删除、修改都会对代码进行加锁,唯独获取数据的时候没有加锁,这样子就使得读写分离了。一般来讲我们在使用的时候,会用一个线程向容器中添加元素,一个线程来读取元素;那么就会出现一个问题:因为在写入数据的同时,需要拷贝一个新的数组,那么在读取的时候可能读取到是旧数据;与此相反的是如果写入数据的速度要先于读取的速度的,这个时候读取到的数据就是最新的数据,那么这里就会有两种情况了。综合起来我们可以分为以下几点:

  • 如果写操作未完成,那么直接读取原数组的数据;
  • 如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
  • 如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

总结

  • 由于写操作的时候需要拷贝数组,会消耗内存,如果原数组内容比较多的情况下,可能导致full GC
  • 不能用于时时读的场景,像拷贝数组,新增元素都需要时间;虽然 能做到最终一致性,但是没法满足实时性要求
  • 适合读多写少的场景,不过因为没法保证到底要放置多少数据,万一数据有点多的话,每次的操作(add/set/remove)都要重新复制数组,代价有点高。
  • 虽然有这么多的缺点,但是它提供了一种读写分离、最终一致性的思想。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值