CopyOnWriteArrayList介绍

发布时间:2024年01月19日

二、CopyOnWriteArrayList

2.1 CopyOnWriteArrayList介绍

CopyOnWriteArrayList是一个线程安全的ArrayList。

CopyOnWriteArrayList是基于lock锁和数组副本的形式去保证线程安全。

在写数据时,需要先获取lock锁,需要复制一个副本数组,将数据插入到副本数组中,将副本数组赋值给CopyOnWriteArrayList中的array。

因为CopyOnWriteArrayList每次写数据都要构建一个副本,如果你的业务是写多,并且数组中的数据量比较大,尽量避免去使用CopyOnWriteArrayList,因为这里会构建大量的数组副本,比较占用内存资源。

CopyOnWriteArrayList是弱一致性的,写操作先执行,但是副本还有落到CopyOnWriteArrayList的array属性中,此时读操作是无法查询到的。

2.2 核心属性&方法

主要查看2个核心属性,以及2个核心方法,还有无参构造

/** 写操作时,需要先获取到的锁资源,CopyOnWriteArrayList全局唯一的。 */
final transient ReentrantLock lock = new ReentrantLock();

/** CopyOnWriteArrayList真实存放数据的位置,查询也是查询当前array */
private transient volatile Object[] array;

// 获取array属性
final Object[] getArray() {
    return array;
}

// 替换array属性
final void setArray(Object[] a) {
    array = a;
}

/**
 *  默认new的CopyOnWriteArrayList数组长度为0。
 *  不像ArrayList,初始长度是10,每次扩容1/2, CopyOnWriteArrayList不存在这个概念
 *  每次写的时候都会构建一个新的数组
 */
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

2.3 读操作

CopyOnWriteArrayList的读操作就是get方法,基于数组索引位置获取数据。

方法之所以要差分成两个,是因为CopyOnWriteArrayList中在获取数据时,不单单只有一个array的数组需要获取值,还有副本中数据的值。

// 查询数据时,只能通过get方法查询CopyOnWriteArrayList中的数据
public E get(int index) {
    // getArray拿到array数组,调用get方法的重载
    return get(getArray(), index);
}
// 执行get(int)时,内部调用的方法
private E get(Object[] a, int index) {
    // 直接拿到数组上指定索引位置的值
    return (E) a[index];
}

2.4 写操作

CopyOnWriteArrayList是基于lock锁和副本数组的形式保证线程安全。

// 写入元素,不指定索引位置,直接放到最后的位置
public boolean add(E e) {
    // 获取全局锁,并执行lock
    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;
        // 将副本数组,赋值给CopyOnWriteArrayList的原数组
        setArray(newElements);
        // 添加成功,返回true
        return true;
    } finally {
        // 释放锁~
        lock.unlock();
    }
}

// 写入元素,指定索引位置。(不会覆盖数据)
public void add(int index, E element) {
    // 拿锁,加锁~
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 获取原数组,还获取了原数组的长度
        Object[] elements = getArray();
        int len = elements.length;
        // 如果索引位置大于原数组的长度,或者索引位置是小于0的。
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        // 声明了副本数组
        Object[] newElements;
        // 原数组长度 - 索引位置等到numMoved
        int numMoved = len - index;
        // 如果numMoved为0,说明数据要放到最后面的位置
        if (numMoved == 0)
            // 直接走了原生态的方式,正常复制一份副本数组
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            // 数组要插入的位置不是最后一个位置
            // 副本数组长度依然是原长度 + 1
            newElements = new Object[len + 1];
            // 将原数组从0索引位置开始复制,复制到副本数组中的前置位置
            System.arraycopy(elements, 0, newElements, 0, index);
            // 将原数组从index位置开始复制,复制到副本数组的index + 1往后放。
            // 这时,index就空缺出来了。
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        // 数据正常放到指定的索引位置
        newElements[index] = element;
        // 将副本数组,赋值给CopyOnWriteArrayList的原数组
        setArray(newElements);
    } finally {
        // 释放锁
        lock.unlock();
    }
}

2.5 移除数据

关于remove操作,要分析两个方法

  • 基于索引位置移除指定数据

  • 基于具体元素删除数组中最靠前的数据

    • 当前这种方式,嵌套了一层,导致如果元素存在话,成本是比较高的。
    • 如果元素不存在,这种设计不需要加锁,提升写的效率
// 删除指定索引位置的数据
public E remove(int index) {
    // 拿锁,加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 获取原数组和原数组长度
        Object[] elements = getArray();
        int len = elements.length;
        // 通过get方法拿到index位置的数据
        E oldValue = get(elements, index);
        // 声明numMoved
        int numMoved = len - index - 1;
        // 如果numMoved为0,说明删除的元素是最后的位置
        if (numMoved == 0)
            // Arrays.copyOf复制一份新的副本数组,并且将最后一个数据不要了
            // 基于setArray将副本数组赋值给array原数组
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            // 删除的元素不在最后面的位置
            // 声明副本数组,长度是原数组长度 - 1
            Object[] newElements = new Object[len - 1];
            // 从0开始复制的index前面
            System.arraycopy(elements, 0, newElements, 0, index);
            // 从index后面复制到最后
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        // 返回被干掉的数据
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

// 删除元素(最前面的)
public boolean remove(Object o) {
    // 没加锁!!!!
    // 获取原数组
    Object[] snapshot = getArray();
    // 用indexOf获取元素在数组的哪个索引位置
    // 没找到的话,返回-1
    int index = indexOf(o, snapshot, 0, snapshot.length);
    // 如果index < 0,说明元素没找到,直接返回false,告辞
    // 如果找到了元素的位置,直接执行remove方法的重载
    return (index < 0) ? false : remove(o, snapshot, index);
}
// 执行remove(Object o),找到元素位置时,执行当前方法
private boolean remove(Object o, Object[] snapshot, int index) {
    // 拿锁,加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 拿到原数组和长度
        Object[] current = getArray();
        int len = current.length;
        // findIndex: 是给if起标识,break 标识的时候,直接跳出if的代码块~~
        if (snapshot != current) findIndex: {
            // 如果没进到if,说明数组没变化,按照原来的index位置删除即可
            // 进到这,说明数组有变化,之前的索引位置不一定对
            // 拿到index位置和原数组长度的值
            int prefix = Math.min(index, len);
            // 循环判断,数组变更后,是否影响到了要删除元素的位置
            for (int i = 0; i < prefix; i++) {
                // 如果不相等,说明元素变化了。
                // 同时判断变化的元素是否是我要删除的元素o
                if (current[i] != snapshot[i] && eq(o, current[i])) {
                    // 如果满足条件,说明当前位置就是我要删除的元素
                    index = i;
                    break findIndex;
                }
            }
            // 如果for循环结束,没有退出if,说明元素可能变化了,总之没找到要删除的元素
            // 如果删除元素的位置,已经大于等于数组长度了。
            if (index >= len)
                // 超过索引范围了,没法删除了。
                return false;
            // 索引还在范围内,判断是否是还是原位置,如果是,直接跳出if代码块
            if (current[index] == o)
                break findIndex;
            // 重新找元素在数组中的位置
            index = indexOf(o, current, index, len);
            // 找到直接跳出if代码块
            // 没找到。执行下面的return false
            if (index < 0)
                return false;
        }
        // 删除套路,构建新数组,复制index前的,复制index后的
        Object[] newElements = new Object[len - 1];
        System.arraycopy(current, 0, newElements, 0, index);
        System.arraycopy(current, index + 1,
                         newElements, index,
                         len - index - 1);
        // 复制到array
        setArray(newElements);
        // 返回true,删除成功
        return true;
    } finally {
        lock.unlock();
    }
}

2.6 覆盖数据&清空集合

覆盖数据就是set方法,可以将指定位置的数据替换

// 覆盖数据
public E set(int index, E element) {
    // 拿锁,加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 获取原数组
        Object[] elements = getArray();
        // 获取原数组的原位置数据
        E oldValue = get(elements, index);

        // 原数据和新数据不一样
        if (oldValue != element) {
            // 拿到原数据的长度,复制一份副本。
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            // 将element替换掉副本数组中的数据
            newElements[index] = element;
            // 写回原数组
            setArray(newElements);
        } else {
            // 原数据和新数据一样,啥不干,把拿出来的数组再写回去
            setArray(elements);
        }
        // 返回原值
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

清空就是清空了~~~

public void clear() {
	// 加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
		// 扔一个长度为0的数组
        setArray(new Object[0]);
    } finally {
        lock.unlock();
    }
}

2.7 迭代器

用ArrayList时,如果想在遍历的过程中去移除或者修改元素,必须使用迭代器才可以。

但是CopyOnWriteArrayList这哥们即便用了迭代器也不让做写操作

不让在迭代时做写操作是因为不希望迭代操作时,会影响到写操作,还有,不希望迭代时,还需要加锁。

// 获取遍历CopyOnWriteArrayList的iterator。
public Iterator<E> iterator() {
    // 其实就是new了一个COWIterator对象,并且获取了array,指定从0开始遍历
    return new COWIterator<E>(getArray(), 0);
}

static final class COWIterator<E> implements ListIterator<E> {
    /** 遍历的快照 */
    private final Object[] snapshot;
    /** 游标,索引~~~ */
    private int cursor;

    // 有参构造
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    // 有没有下一个元素,基于遍历的索引位置和数组长度查看
    public boolean hasNext() {
        return cursor < snapshot.length;
    }
    // 有没有上一个元素
    public boolean hasPrevious() {
        return cursor > 0;
    }

    // 获取下一个值,游标动一下
    public E next() {
        // 确保下个位置有数据
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    // 获取上一个值,游标往上移动
    public E previous() {
        if (! hasPrevious())
            throw new NoSuchElementException();
        return (E) snapshot[--cursor];
    }

    // 拿到下一个值的索引,返回游标
    public int nextIndex() {
        return cursor;
    }

    // 拿到上一个值的索引,返回游标
    public int previousIndex() {
        return cursor-1;
    }

    // 写操作全面禁止!!
    public void remove() {
        throw new UnsupportedOperationException();
    }

  
    public void set(E e) {
        throw new UnsupportedOperationException();
    }

  
    public void add(E e) {
        throw new UnsupportedOperationException();
    }

    // 兼容函数式编程
    @Override
    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        Object[] elements = snapshot;
        final int size = elements.length;
        for (int i = cursor; i < size; i++) {
            @SuppressWarnings("unchecked") E e = (E) elements[i];
            action.accept(e);
        }
        cursor = size;
    }
}
文章来源:https://blog.csdn.net/m0_63694520/article/details/135707523
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。