CopyOnWriteArrayList源码分析

CopyOnWriteArrayList 简介

CopyOnWriteArrayList是一种线程安全的ArrayList,底层是基于数组实现。

Copy-On-Write 的思想是什么

"Copy-On-Write"即写时复制:需要修改( addsetremove 等操作) CopyOnWriteArrayList 的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。

缺点:

  • 内存占用:每次写操作都需要复制一份原始数据,会占用额外的内存空间,在数据量比较大的情况下,可能会导致内存资源不足。

  • 写操作开销:每一次写操作都需要复制一份原始数据,然后再进行修改和替换,所以写操作的开销相对较大,在写入比较频繁的场景下,性能可能会受到影响。

  • 数据一致性问题:修改操作不会立即反映到最终结果中,还需要等待复制完成,这可能会导致一定的数据一致性问题。

CopyOnWriteArrayList 源码分析

  • List : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。

  • RandomAccess :这是一个标志接口,表明实现这个接口的 List 集合是支持 快速随机访问 的。

  • Cloneable :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。

  • Serializable : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。

初始化

CopyOnWriteArrayList 中有一个无参构造函数和两个有参构造函数。

scss 复制代码
// 创建一个空的 CopyOnWriteArrayList
public CopyOnWriteArrayList() {
    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();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        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));
}

插入元素

CopyOnWriteArrayListadd()方法有三个版本:

  • add(E e):在 CopyOnWriteArrayList 的尾部插入元素。
  • add(int index, E element):在 CopyOnWriteArrayList 的指定位置插入元素。
  • addIfAbsent(E e):如果指定元素不存在,那么添加该元素。如果成功添加元素则返回 true。

依add(E e)为例:

csharp 复制代码
// 插入元素到 CopyOnWriteArrayList 的尾部
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);
        // 元素放在新数组末尾
        newElements[len] = e;
        // array指向新数组
        setArray(newElements);
        return true;
    } finally {
        // 解锁
        lock.unlock();
    }
}
  1. 先使用ReentrantLock加锁,保证线程安全。
  2. 再创建一个新数组,长度是原数组长度+1,并把原数组元素拷贝到新数组里面。
  3. 然后在新数组末尾位置赋值
  4. 使用新数组替换掉原数组
  5. 最后释放锁

读取元素

CopyOnWriteArrayList 的读取操作是基于内部数组 array 并没有发生实际的修改,因此在读取操作时不需要进行同步控制和锁操作,可以保证数据的安全性。这种机制下,多个线程可以同时读取列表中的元素。

typescript 复制代码
// 底层数组,只能通过getArray和setArray方法访问
private transient volatile Object[] array;

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

final Object[] getArray() {
    return array;
}

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

不过,get方法是弱一致性的,在某些情况下可能读到旧的元素值。

get(int index)方法是分两步进行的:

  1. 通过getArray()获取当前数组的引用;
  2. 直接从数组中获取下标为 index 的元素。

这个过程并没有加锁,所以在并发环境下可能出现如下情况:

  1. 线程 1 调用get(int index)方法获取值,内部通过getArray()方法获取到了 array 属性值;
  2. 线程 2 调用CopyOnWriteArrayListaddsetremove 等修改方法时,内部通过setArray方法修改了array属性的值;
  3. 线程 1 还是从旧的 array 数组中取值。

获取列表中元素的个数

csharp 复制代码
public int size() {
    return getArray().length;
}

CopyOnWriteArrayList中的array数组每次复制都刚好能够容纳下所有元素,并不像ArrayList那样会预留一定的空间。因此,CopyOnWriteArrayList中并没有size属性CopyOnWriteArrayList的底层数组的长度就是元素个数,因此size()方法只要返回数组长度就可以了

删除元素

CopyOnWriteArrayList删除元素相关的方法一共有 4 个:

  1. remove(int index):移除此列表中指定位置上的元素。将任何后续元素向左移动(从它们的索引中减去 1)。
  2. boolean remove(Object o):删除此列表中首次出现的指定元素,如果不存在该元素则返回 false。
  3. boolean removeAll(Collection<?> c):从此列表中删除指定集合中包含的所有元素。
  4. void clear():移除此列表中的所有元素。

这里以remove(int index)为例进行介绍:

ini 复制代码
public E remove(int index) {
    // 获取可重入锁
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
    	   //获取当前array数组
        Object[] elements = getArray();
        // 获取当前array长度
        int len = elements.length;
        //获取指定索引的元素(旧值)
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        // 判断删除的是否是最后一个元素
        if (numMoved == 0)
        	   // 如果删除的是最后一个元素,直接复制该元素前的所有元素到新的数组
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            // 分段复制,将index前的元素和index+1后的元素复制到新数组
            // 新数组长度为旧数组长度-1
            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;
    } finally {
       	// 解锁
        lock.unlock();
    }
}
  1. 先使用ReentrantLock加锁,保证线程安全。
  2. 再创建一个新数组,长度是原数组长度-1,并把原数组中剩余元素(不包含需要删除的元素)拷贝到新数组里面。
  3. 使用新数组替换掉原数组
  4. 最后释放锁

判断元素是否存在

CopyOnWriteArrayList提供了两个用于判断指定元素是否在列表中的方法:

  • contains(Object o):判断是否包含指定元素。
  • containsAll(Collection<?> c):判断是否保证指定集合的全部元素。
typescript 复制代码
// 判断是否包含指定元素
public boolean contains(Object o) {
    //获取当前array数组
    Object[] elements = getArray();
    //调用index尝试查找指定元素,如果返回值大于等于0,则返回true,否则返回false
    return indexOf(o, elements, 0, elements.length) >= 0;
}

// 判断是否保证指定集合的全部元素
public boolean containsAll(Collection<?> c) {
    //获取当前array数组
    Object[] elements = getArray();
    //获取数组长度
    int len = elements.length;
    //遍历指定集合
    for (Object e : c) {
        //循环调用indexOf方法判断,只要有一个没有包含就直接返回false
        if (indexOf(e, elements, 0, len) < 0)
            return false;
    }
    //最后表示全部包含或者制定集合为空集合,那么返回true
    return true;
}

参考文章:

1.CopyOnWriteArrayList 源码分析 | JavaGuide(Java面试 + 学习指南) 2.阿里Java面试官:CopyOnWriteArrayList底层是怎么保证线程安全的? - 掘金 (juejin.cn)

相关推荐
程序员爱钓鱼32 分钟前
Go语言实战案例-创建模型并自动迁移
后端·google·go
javachen__37 分钟前
SpringBoot整合P6Spy实现全链路SQL监控
spring boot·后端·sql
uzong6 小时前
技术故障复盘模版
后端
GetcharZp7 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程7 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研7 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi7 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国8 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy8 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack9 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt