Java并发系列源码分析(六)--CopyOnWriteArrayList

CopyOnWriteArrayList是一个线程安全的集合,通过写时复制策略保证并发操作时的数据一致性。它在添加、删除元素时复制旧数组到新数组,避免并发修改。ReentrantLock用于防止多线程同时操作,确保数据安全。get方法直接从旧数组获取元素,读操作看到的是旧数据,可能会有短暂不一致。CopyOnWriteArraySet基于此实现,不允许重复元素且无序。
摘要由CSDN通过智能技术生成

简介

CopyOnWriterArrayList是一个写时复制的集合,顾名思义就是在对集合中的数组元素操作的时候会将旧数组中的元素复制一份到新的数组中去,然后对新的数组进行操作,此时旧数组中的元素只能被读取,而不能进行操作,当新的数组操作完成之后会将新数组设置成当前集合中使用的数组。

常量

/** 重入锁,防止多个线程同时操作 */
final transient ReentrantLock lock = new ReentrantLock();/** 当前集合中使用的数组对象 */
private transient volatile Object[] array;
  • ReentrantLockReentrantLock的作用主要是防止多个线程同时对集合进行操作,因为在一些方法中对元素操作的时候会复制一份新的数组元素进行操作,如果说多个线程同时操作的时候就会导致有多个不相同的数组,从而引发数据丢失。
  • array:当前集合中使用的数组对象,每次对元素操作的时候,数组对象都会改变。

构造方法

/**
 * 创建一个空集合
 */
public CopyOnWriteArrayList() {
    //创建一个长度为0的数组对象,并将数组对象设置为当前集合中使用的数组
    setArray(new Object[0]);
}/**
 * 根据指定的集合中的元素来创建新的集合CopyOnWriteArrayList
 */
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    //校验指定集合的类型是否与当前要创建的集合的类型相同
    //如果相同则获取指定集合中的数组对象,并将数组对象设置为当前集合中使用的数组对象
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        //获取指定集合中的数组对象
        elements = c.toArray();
        if (c.getClass() != java.util.ArrayList.class)
            //将指定集合中的数组对象的元素全部拷贝到Object类型的新数组中
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    //将数组对象设置为当前集合中使用的数组对象
    setArray(elements);
}/**
 * 使用指定的数组对象中的元素创建一个新的集合
 */
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

在创建集合的时候没有指定数组或者集合的时候则会创建一个空的数组对象,如果指定了则会使用指定的集合中的数组或指定的数组,并将数组中的元素拷贝到一个新的数组对象中,并将新的数组对象设置为当前集合中使用的数组对象。

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)
            //添加的元素的指定索引位置大于数组的长度或小于0则抛出异常
            throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len);
        //新数组
        Object[] newElements;
        //获取idnex索引位置后续需要移动的元素个数
        int numMoved = len - index;
        if (numMoved == 0)
            //需要移动的元素个数为0则说明添加的元素所在的idnex索引位置是数组末尾的索引位置
            //此时需要将旧数组中的所有元素拷贝到新的数组中
            //而新的数组的长度为旧数组的长度+1
            //旧数组长度+1的位置则是放置新添加的元素
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            //创建一个新的数组
            newElements = new Object[len + 1];
            //将旧数组中0到idnex索引位置的元素拷贝到新的数组中
            System.arraycopy(elements, 0, newElements, 0, index);
            //将旧数组中index到len-1的索引位置的元素拷贝到新的数组中的index+1的索引位置
            System.arraycopy(elements, index, newElements, index + 1, numMoved);
        }
        //在指定的索引位置上添加指定的元素
        newElements[index] = element;
        //将新的数组设置为当前集合中使用的数组对象
        setArray(newElements);
    } finally {
        //释放锁
        lock.unlock();
    }
}

add方法中首先会获取全局的重入锁对象并且加锁,通过len-index获取到从index开始的索引位置以及后续的索引位置需要移动的元素个数,因为指定要添加到index的索引位置上,所以index以及后续的索引位置上的元素都要往后移动。

当需要移动的元素个数为0则说明添加的元素所在的位置则是数组的尾部,此时不要移动元素,只需要将旧数组中的所有元素拷贝到新的数组中去,并且新的数组的长度是比旧数组的长度多一个索引位置的,多出的一个索引位置则是存放指定添加的元素的,当旧数组中的元素都拷贝到了新的数组中的时候则会将需要添加的元素放置到新数组中多出的一个索引位置上。

当需要移动的元素个数大于0的时候则需要先将旧数组中index索引位置前面的索引元素都拷贝到新数组中,然后再将index以及后续的索引位置的元素都拷贝到新数组中,然后在index索引位置上添加指定的元素,并将新数组设置为当前集合中使用的数组。

remove

public boolean remove(Object o) {
    //获取当前集合中使用的数组对象作为数组快照
    Object[] snapshot = getArray();
    //从数组快照中查询指定的元素o所在的索引位置
    int index = indexOf(o, snapshot, 0, snapshot.length);
    //从集合中删除元素
    return (index < 0) ? false : remove(o, snapshot, index);}private boolean remove(Object o, Object[] snapshot, int index) {
    //获取锁
    final ReentrantLock lock = this.lock;
    //加锁
    lock.lock();
    try {
        //获取当前集合中的数组对象
        Object[] current = getArray();
        //获取数组长度
        int len = current.length;
        //校验数组快照与当前获取到的数组是否是同一个数组
        if (snapshot != current) findIndex: {
            //获取有效的值
            //index最小,则说明当前集合中的数组对象中的元素未被删除或者说删除的元素较少
            //len最小,则说明当前集合中的数组对象中的元素删除的比较多,已经小于了当前要删除的元素所在的索引位置
            int prefix = Math.min(index, len);
            for (int i = 0; i < prefix; i++) {
                //先校验数组快照中的元素是否与当前集合中的数组对象的元素相同
                //如果都相同则说明没有线程去更改当前集合中的数组元素
                //如果不相同则校验待删除的元素是否与当前集合中的数组元素相同
                //如果相同则说明有线程修改了当前集合中的数组元素
                //此时就需要纠正原先获取到的索引位置并退出循环以及if语句
                if (current[i] != snapshot[i] && eq(o, current[i])) {
                    index = i;
                    break findIndex;
                }
            }
            //两个数组中从0到prefix索引位置的元素都相同
            if (index >= len)
                //待删除的元素已经不存在了
                return false;
            if (current[index] == o)
                //index索引位置的元素与待删除的元素相同则退出if语句
                break findIndex;
            //重新从当前集合中的数组中获取待删除的元素所在的索引位置
            index = indexOf(o, current, index, len);
            if (index < 0)
                return false;
        }
        //创建一个新的数组
        //新数组的长度是旧数组的长度-1
        Object[] newElements = new Object[len - 1];
        //将旧数组中待删除的元素所在的索引位置的前面所有的元素都拷贝到新的数组中
        System.arraycopy(current, 0, newElements, 0, index);
        //将旧数组中待删除的元素所在的索引位置的后面所有的元素都拷贝到新的数组中
        System.arraycopy(current, index + 1, newElements, index, len - index - 1);
        //将新数组设置为当前集合所使用的数组对象
        setArray(newElements);
        return true;
    } finally {
        //释放锁
        lock.unlock();
    }
}

remove方法会在加锁之前获取数组快照,然后通过数组快照确定待删除的元素所在的索引位置,在加锁之后会再次获取数组对象,如果说加锁之前的数组快照和加锁之后的数组相同,那就将旧数组index之前以及后续的索引位置上的元素拷贝到新数组中去,拷贝的索引位置的元素不包含index
如果说两个数组不是同一个数组,那就要看一下加锁之后的数组中是否包含待删除的元素,如果包含则会进行删除,不包含则说明已经被其它线程删除了。

get

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

get方法是直接通过当前集合中的数组来获取元素的,当有线程对当前集合中的数组进行添加元素的操作的时候,获取元素则会从旧的数组中获取,而添加元素的则是对新的数组操作,将旧数组中的元素拷贝一份到新的数组中,并将添加的元素添加到新的数组中。

总结

CopyOnWriteArrayList中的方法的代码都比较简单,都是通过数组拷贝的方法来添加新的元素,可能造成短暂的数据不一致性,因为读是对旧数组进行操作的,而写是对新数组操作的,新写的数据可能不会立马被访问到,而CopyOnWriteArrayList中的迭代器是不能对数组中的元素进行remove、set、add操作的,因为迭代器是依靠数组快照来执行的,不能保证数组快照是最新的数组对象,如果数组快照不是最新的数组对象的时候,使用迭代器对数组快照进行remove、set、add其中一个操作就会导致操作之后的元素不能同步到新的数组中,只能保存在数组快照中,当前迭代器执行完毕之后,数组快照就会被清理,而操作的元素也会丢失。

CopyOnWriteArraySet集合底层就是用的CopyOnWriteArrayList,CopyOnWriteArraySet是一个无序且元素不能重复的集合,在添加元素的时候会先调用indexof方法来获取元素的索引,如果索引大于等于0则说明已经存在了,此时就不能添加该元素,当元素不存在的时候才能进行添加操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值