01.JUC 集合 - CopyOnWriteArrayList

基本概念

CopyOnWriteArrayList 的实现原理实际与 ArrayList 大同小异。它们内部都通过维护一个数组来对数据进行操作。

不同的是它的处理写操作(包括 add、set、remove)是先将原始的数据通过Arrays.copyof 方法复制生成一份新的数组。然后在新的数据对象上进行写操作,写完后再将原来的引用指向到当前这个数据对象。这样保证了每次写都是在新的对象上。而 ArrayList 除非是内部数组需要扩容操作才会复制生成一份新的数组。

为了保证写操作的一致性,这里要对各种写操作要加一把重入锁(ReentrantLock)来控制。


内部构造

  • 成员变量,在 CopyOnWriteArrayList 中使用 array 表示它内部维护的数组 ,该数组用于存放数据。
private volatile transient Object[] array;

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

final Object[] getArray() {
    return array;
}
  • 构造函数,CopyOnWriteArrayList 在被构造时,默认会创建一个容量为 0 的数组。值得注意的是,数组容量是固定的,但是对该类的每个操作都会通过复制创建新的数组,并且能在复制过程完成数组的容量变更。
public CopyOnWriteArrayList() {
    // 关键 -> 为 0 是因为对数组的操作都会通过复制创建新的数组,一开始不用确定数组容量
    setArray(new Object[0]);
}

原理分析

1.添加操作

在 CopyOnWriteArrayList 类中,存在两种添加操作:指定位置、不指定位置。

添加操作使用该类内部的 ReentrantLock 来控制并发,该锁是独占锁,说明同一时间有且只有一个线程能对该类执行该操作。

单个元素的添加操作默认会创建(容量+1)的新数组,并且从旧数组复制所有元素。由于这个特性,导致了该类的写操作需要大面积复制数组,所以性能肯定很差

下面来看两种操作的实现过程:

  • 添加元素时不指定位置
// 内部维护一个重入锁。
transient final ReentrantLock lock = new ReentrantLock();

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;

        // 创建[容量+1]新数组并,从旧数组复制所有元素
        Object[] newElements = Arrays.copyOf(elements, len + 1);

        // 添加到数组末尾;数组容量为 len+1 时,下标为 0 - len。
        newElements[len] = e;

        // 更新内部数组
        setArray(newElements);
        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;
        if (index > len || index < 0){
            // 抛出异常...
        }

        Object[] newElements;

        // 需要移动的元素,为 0 表示直接放置在数组末尾
        int numMoved = len - index;
        if (numMoved == 0){
            newElements = Arrays.copyOf(elements, len + 1);
        }else {
            newElements = new Object[len + 1];

            // 复制 [0-index] 的元素到新数组的[0-index]
            System.arraycopy(elements, 0, newElements, 0, index);

            // 复制 [index-末尾]的元素到新数组的 [(index+1)-末尾]
            // 空出 index 位置用来添加新元素
            System.arraycopy(elements, index, newElements, index + 1, numMoved);
        }
        newElements[index] = element;
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}

2.查询操作

在 CopyOnWriteArrayList 类中,存在两种查询操作:查询指定位置的元素、查询指定的元素。
后者比前者多了一步查询元素位置的过程。

需要注意的是:在该类中只有查询操作不需要加锁。

下面来看实现过程:

  • 查询指定位置的元素
public E get(int index) {
    return get(getArray(), index);
}

@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}
  • 查询指定的元素
public boolean contains(Object o) {
    Object[] elements = getArray();
    return indexOf(o, elements, 0, elements.length) >= 0;
}

private static int indexOf(Object o, Object[] elements, int index, int fence) {
    // 区分是不是 null,该方法的原理与 Arraylist 一样。
    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;
}

3.修改操作

需要注意的是,虽然修改操作不改变数组的容量,但也会创建新的数组并从旧数组复制元素。

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);
            newElements[index] = element;
            setArray(newElements);
        } else {
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

4.删除操作

该类的删除操作分为:删除指定位置的元素、删除指定元素,但是实现过程却大相径庭。下面来看实现过程:

  • 删除指定位置的元素
public E remove(int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 取得该位置的值,用于返回
        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();
    }
}
  • 删除指定元素
public boolean remove(Object o) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;

        if (len != 0) {
            int newlen = len - 1;
            Object[] newElements = new Object[newlen];

            // ①比较 [0 - (newlen -1 = len -2)] 位置上的元素
            for (int i = 0; i < newlen; ++i) {
                // 比较 o1,o2
                if (eq(o, elements[i])) {
                    // 相等,则通过复制,将元素前移,覆盖掉 i 位置上的元素。
                    for (int k = i + 1; k < len; ++k){
                        newElements[k - 1] = elements[k];
                    }   
                    setArray(newElements);
                    return true;
                } else{
                    // 不相等执行复制操作
                    newElements[i] = elements[i];
                }
            }

            // ②比较 [newlen = len -1]位置上的元素,说明指定元素在数组末尾
            if (eq(o, elements[newlen])) {
                setArray(newElements);
                return true;
            }
        }
        return false;
    } finally {
        lock.unlock();
    }
}

// 比较 01,02 是否相等
private static boolean eq(Object o1, Object o2) {
    return (o1 == null ? o2 == null : o1.equals(o2));
}

总结

在 CopyOnWriteArrayList 中添加、修改、删除都属于写操作,查询属于读操作。关于读写问题,具有以下特性:

  • 写操作:需要大面积复制数组,而且需要锁保证数据的一致性,所以性能肯定很差。

  • 读操作:因为操作的对象和写操作不是同一个对象,而且读操作的线程之间也不需要加锁。

  • 读写同步:读和写之间的同步处理只是在写完后通过一个简单的 = 将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合在多线程里使用,绝对不会发生 ConcurrentModificationException

综上所述, CopyOnWriteArrayList 适合使用在读操作远远大于写操作的场景里,比如缓存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oxf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值