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)

相关推荐
神奇小汤圆12 分钟前
浅析二叉树、B树、B+树和MySQL索引底层原理
后端
文艺理科生21 分钟前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling22 分钟前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅24 分钟前
springBoot项目有几个端口
java·spring boot·后端
Luke君6079726 分钟前
Spring Flux方法总结
后端
define952729 分钟前
高版本 MySQL 驱动的 DNS 陷阱
后端
忧郁的Mr.Li1 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
暮色妖娆丶2 小时前
SpringBoot 启动流程源码分析 ~ 它其实不复杂
spring boot·后端·spring
Coder_Boy_2 小时前
Deeplearning4j+ Spring Boot 电商用户复购预测案例中相关概念
java·人工智能·spring boot·后端·spring
Java后端的Ai之路2 小时前
【Spring全家桶】-一文弄懂Spring Cloud Gateway
java·后端·spring cloud·gateway