【Java 并发编程】CopyOnWriterArrayList 详解

news2025/1/3 2:28:51

CopyOnWriterArrayList 详解

  • 1. ArrayList
    • 1.1 ArrayList 和 LinkedList 的区别
    • 1.2 ArrayList 如何保证线程安全
  • 2. CopyOnWriteArrayList 原理
  • 3. CopyOnWriteArrayList 的优缺点
    • 3.1 优点
    • 3.2 缺点
  • 4. 源码分析
    • 4.1 两个成员变量
    • 4.2 构造函数
    • 4.3 add(E e)
    • 4.4 add(int index, E element)
    • 4.5 set(int index, E element)
    • 4.6 remove(int index)
    • 4.7 get(int index)
    • 4.8 size()
    • 4.9 迭代器
  • 6. 迭代器的 fail-fast 与 fail-safe 机制
    • 6.1 fail-fast 机制
      • fail-fast 解决方案
    • 6.2 fail-safe 机制
      • 缺点:
  • 5. 总结

1. ArrayList

1.1 ArrayList 和 LinkedList 的区别

  • ArrayList:底层是数组实现,线程不安全,查询和修改非常快,但是增加和删除慢。

  • LinkedList:底层是双向链表,线程不安全,查询和修改速度慢,但是增加和删除速度快。

1.2 ArrayList 如何保证线程安全

JDK 建议我们,如果有多个线程并发访问 ArrayList 实例,并且至少有一个线程做修改操作(可能会引起线程安全问题,具体表现为 ConcurrentModificationException 异常),要自己加锁或者使用 Collections.synchronizedList 方法。

在这里插入图片描述
其实,在 Java 并发包下实现了线程安全的 ArrayList - CopyOnWriteArrayList

2. CopyOnWriteArrayList 原理

CopyOnWriteArrayList 适用于读多写少的并发场景,其内部适用写时复制的机制,它的核心思想是在写操作时,先创建一个原集合的副本(即拷贝一份原始数组),然后在副本上进行修改操作,最后将修改后的副本替换原集合。这样可以保证在写操作期间,其他线程仍然可以安全地读取原集合的数据(读操作返回的结果可能不是最新的),不会受到并发修改的影响。

在这里插入图片描述

CopyOnWriteArrayList 在结构上与 ArrayList 类似(内部都是使用数组来存储元素),但在线程安全性和写操作的代价上有所不同,基本会分为4步:

  • 加锁;

  • 从原数组中拷贝出新数组;

  • 在新数组上进行操作,并把新数组赋值给数组容器;

  • 释放锁。

除了加锁之外,CopyOnWriteArrayList 的底层数组还被 volatile 关键字修饰,意味着一旦数组被修改,其它线程立马能够感知到:

private transient volatile Object[] array;

总结:CopyOnWriteArrayList 的设计思想是:读写分离 + 最终一致,并利用 锁 + 数组拷贝 + volatile 关键字保证了 List 的线程安全。

3. CopyOnWriteArrayList 的优缺点

3.1 优点

  1. 线程安全:CopyOnWriteArrayList 在写操作时采用写时复制的策略,即创建一个副本进行修改,这保证了写操作的安全性。其他线程可以继续读取原集合的数据,不会受到写操作的影响。

  2. 高效的读操作:读取操作不需要加锁或同步,多个线程可以同时读取 CopyOnWriteArrayList,不会出现读取冲突的情况。

CopyOnWriteArrayList 适用于读多写少的并发环境,例如缓存、观察者模式等场景。在这些场景中,读操作远远多于写操作,CopyOnWriteArrayList 可以提供较好的性能和线程安全性。

3.2 缺点

  1. 内存占用:由于每次写操作都会创建一个新的数组,CopyOnWriteArrayList 会消耗更多的内存空间。如果集合中的元素较多或写操作频繁,可能导致 young gc 或者 full gc。

  2. 数据一致性:CopyOnWriteArrayList 的写操作是通过创建副本数组来实现的,因此在写操作过程中,读取操作仍然访问原始数组。这意味着在写操作期间,读取操作可能无法立即反映最新的修改。这种数据一致性的延迟可能会对某些应用场景产生影响。

CopyOnWriteArrayList 不适用于写操作过多、实时性要求高的场景,需要根据具体的应用场景进行权衡和选择。

4. 源码分析

4.1 两个成员变量

// 可重入锁,用于对写操作加锁
final transient ReentrantLock lock = new ReentrantLock();

// Object类型数组,存放数据,volatile 修饰,目的是一个线程对这个字段的修改另外一个线程立即可见
private transient volatile Object[] array;

4.2 构造函数

CopyOnWriteArrayList() 空参构造函数:

public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

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

无参构造函数直接创建了一个长度为 0 的 Object 数组。

CopyOnWriteArrayList(Collection<? extends E> c)

public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class)
        // 如果集合类型就是 CopyOnWriteArrayList,则直接将其 array 赋值给当前 CopyOnWriteArrayList
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        // 如果不是 CopyOnWriteArrayList 类型,则将集合转换为数组
        elements = c.toArray();
        // 就如 ArrayList 源码分析所述那样,c.toArray() 返回类型不一定是 Object[].class,所以需要转换
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    // 设置 array 值
    setArray(elements);
}

CopyOnWriteArrayList(E[] toCopyIn)

public CopyOnWriteArrayList(E[] toCopyIn) {
    // 入参为数组,拷贝一份赋值给 array
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

4.3 add(E e)

add(E e) 往 CopyOnWriteArrayList 末尾添加元素:

public boolean add(E e) {
    // 获取可重入锁
    final ReentrantLock lock = this.lock;
    // 上锁,同一时间内只能有一个线程进入
    lock.lock();
    try {
        // 获取当前 array 属性值
        Object[] elements = getArray();
        // 获取当前 array 数组长度
        int len = elements.length;
        // 复制一份新数组,新数组长度为当前 array 数组长度 +1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 在新数组末尾添加元素
        newElements[len] = e;
        // 新数组赋值给 array 属性
        setArray(newElements);
        return true;
    } finally {
        // 锁释放
        lock.unlock();
    }
}

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

可以看到,add 操作通过 ReentrantLock 来确保线程安全。通过 add 方法,我们也可以看出 CopyOnWriteArrayList 修改操作的基本思想为:复制一份新的数组,新数组长度刚好能够容纳下需要添加的元素;在新数组里进行操作;最后将新数组赋值给 array 属性,替换旧数组。这种思想也称为 “写时复制”,所以称为 CopyOnWriteArrayList。

此外,我们可以看到 CopyOnWriteArrayList 中并没有类似于 ArrayList 的 grow 方法扩容的操作。

4.4 add(int index, E element)

add(int index, E element) 指定下标添加指定元素:

public void add(int index, E element) {
    // 获取可重入锁
    final ReentrantLock lock = this.lock;
    // 上锁,同一时间内只能有一个线程进入
    lock.lock();
    try {
        // 获取当前 array 属性值
        Object[] elements = getArray();
        // 获取当前 array 数组长度
        int len = elements.length;
        // 下标检查
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+", Size: "+len);
        Object[] newElements;
        int numMoved = len - index;
        if (numMoved == 0)
            // numMoved 为 0,说明是在末尾添加,过程和 add(E e) 方法一致
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            // 否则创建一个新数组,数组长度为旧数组长度值 +1
            newElements = new Object[len + 1];
            // 分两次复制,分别将 index 之前和 index+1 之后的元素复制到新数组中
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index, newElements, index + 1, numMoved);
        }
        // 在新数组的 index 位置添加指定元素
        newElements[index] = element;
        // 新数组赋值给 array 属性,替换旧数组
        setArray(newElements);
    } finally {
        // 锁释放
        lock.unlock();
    }
}

4.5 set(int index, E element)

set(int index, E element) 设置指定位置的值:

public E set(int index, E element) {
    // 获取可重入锁
    final ReentrantLock lock = this.lock;
    // 上锁,同一时间内只能有一个线程进入
    lock.lock();
    try {
        // 获取当前 array 属性值
        Object[] elements = getArray();
        // 获取当前 array 指定 index 下标值
        E oldValue = get(elements, index);
        if (oldValue != element) {
            // 如果新值和旧值不相等
            int len = elements.length;
            // 复制一份新数组,长度和旧数组一致
            Object[] newElements = Arrays.copyOf(elements, len);
            // 修改新数组 index 下标值
            newElements[index] = element;
            // 新数组赋值给 array 属性,替换旧数组
            setArray(newElements);
        } else {
            // 即使新值和旧值一致,为了确保 volatile 语义,需要重新设置 array
            setArray(elements);
        }
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

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

4.6 remove(int index)

remove(int index) 删除指定下标元素:

public E remove(int index) {
    // 获取可重入锁
    final ReentrantLock lock = this.lock;
    // 上锁,同一时间内只能有一个线程进入
    try {
        // 获取当前 array 属性值
        Object[] elements = getArray();
        // 获取当前 array 长度
        int len = elements.length;
        // 获取旧值
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
            // 如果删除的是最后一个元素,则将当前 array 设置为新数组
            // 新数组长度为旧数组长度 -1,这样刚好截去了最后一个元素
            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();
    }
}

可以看到,CopyOnWriteArrayList 中的增删改操作都是在新数组中进行的,并且通过加锁的方式确保同一时刻只能有一个线程进行操作,操作完后赋值给 array 属性,替换旧数组,旧数组失去了引用,最终由 GC 回收。

4.7 get(int index)

public E get(int index) {
    return get(getArray(), index);
}
final Object[] getArray() {
    return array;
}

可以看到,get(int index) 操作是分两步进行的:

  1. 通过 getArray() 获取array属性值;
  2. 获取 array 数组 index 下标值。

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

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

所以 get 方法是弱一致性的

4.8 size()

public int size() {
    return getArray().length;
}

size() 方法返回当前 array 属性长度,因为 CopyOnWriteArrayList 中的 array 数组每次复制都刚好能够容纳下所有元素,并不像 ArrayList 那样会预留一定的空间。所以 CopyOnWriteArrayList 中并没有 size 属性,元素的个数和数组的长度是相等的。

4.9 迭代器

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
    /** Snapshot of the array */
    private final Object[] snapshot;
    /** Index of element to be returned by subsequent call to next.  */
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    public boolean hasNext() {
        return cursor < snapshot.length;
    }
    ......
}

可以看到,迭代器也是弱一致性的,并没有在锁中进行。如果其他线程没有对 CopyOnWriteArrayList 进行增删改的操作,那么 snapshot 还是创建迭代器时获取的 array,但是如果其他线程对 CopyOnWriteArrayList 进行了增删改的操作,旧的数组会被新的数组给替换掉,但是 snapshot 还是原来旧的数组的引用:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("hello");
Iterator<String> iterator = list.iterator();
list.add("world");
while (iterator.hasNext()){
    System.out.println(iterator.next());
}

输出结果仅为 hello。

6. 迭代器的 fail-fast 与 fail-safe 机制

在 Java 中,迭代器(Iterator)在迭代的过程中,如果底层的集合被修改(添加或删除元素),不同的迭代器对此的表现行为是不一样的,可分为两类:Fail-Fast(快速失败)和 Fail-Safe(安全失败)。

6.1 fail-fast 机制

fail-fast 机制是 java 集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。例如:当某一个线程A通过 iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。

在 java.util 包中的集合,如 ArrayList、HashMap 等,它们的迭代器默认都是采用 fail-fast 机制。

在这里插入图片描述

fail-fast 解决方案

  • 方案一:在遍历过程中所有涉及到改变 modCount 值的地方全部加上 synchronized 或者直接使用 Collection#synchronizedList,这样就可以解决问题,但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。

  • 方案二:使用 CopyOnWriteArrayList 替换 ArrayList,推荐使用该方案(即fail-safe)。

6.2 fail-safe 机制

任何对集合结构的修改都会在一个复制的集合上进行,因此不会抛出 ConcurrentModificationException。在 java.util.concurrent 包中的集合,如 CopyOnWriteArrayList、ConcurrentHashMap 等,它们的迭代器一般都是采用 Fail-Safe 机制。

缺点:

  • 采用 Fail-Safe 机制的集合类都是线程安全的,但是它们无法保证数据的实时一致性,它们只能保证数据的最终一致性。在迭代过程中,如果集合被修改了,可能读取到的仍然是旧的数据。

  • Fail-Safe 机制还存在另外一个问题,就是内存占用。由于这类集合一般都是通过复制来实现读写分离的,因此它们会创建出更多的对象,导致占用更多的内存,甚至可能引起频繁的垃圾回收,严重影响性能。

5. 总结

  1. CopyOnWriteArrayList 体现了写时复制的思想,增删改操作都是在复制的新数组中进行的;

  2. CopyOnWriteArrayList 的取值方法是弱一致性的,无法确保实时取到最新的数据;

  3. CopyOnWriteArrayList 的增删改方法通过可重入锁确保线程安全;

  4. CopyOnWriteArrayList 线程安全体现在多线程增删改不会抛出java.util.ConcurrentModificationException异常,并不能确保数据的强一致性;

  5. 同一时刻只能有一个线程对 CopyOnWriteArrayList 进行增删改操作,而读操作没有限制,并且 CopyOnWriteArrayList 增删改操作都需要复制一份新数组,增加了内存消耗,所以 CopyOnWriteArrayList 适合读多写少的情况。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1030968.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(总目录)springboot - 实现zip文件上传并对zip文件解压, 包含上传oss

全文目录,一步到位 1.本文概述1.1 本文简介 2. 功能实现2.1 统一文件校验2.2 普通(多)文件上传[服务器]2.2.1 controller层2.2.2 service层2.2.3 业务impl实现类2.2.4 FileIOUtils工具包代码 2.3 zip文件的解压2.4 图片文件的压缩2.5 oss文件后端上传2.6 oss文件前端上传2.7 后…

传统企业如何实现数字化转型?

传统企业实现数字化转型是一个复杂且多方面的过程&#xff0c;涉及将数字技术和战略融入业务的各个方面&#xff0c;以推动创新、效率和竞争力。以下是传统企业实现数字化转型可以遵循的步骤和策略&#xff1a; 1.领导层的认可和愿景&#xff1a; 首先要确保最高领导层&#x…

SpringCloud Gateway搭建Gateway 微服务应用实例

&#x1f600;前言 本篇博文是关于SpringCloud Gateway搭建Gateway 微服务应用实例&#xff0c;希望你能够喜欢 &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以帮助到大家&#xff0c;您…

前端JavaScript入门到精通,javascript核心进阶ES6语法、API、js高级等基础知识和实战 —— JS基础(三)

思维导图 一、循环-for 1.1 for 循环-基本使用 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport"…

scryptTS文档搜索功能上线!

在 scryptTS 文档中搜索 随着 scryptTS 文档的内容越来越丰富&#xff0c;从大量资料中快速定位感兴趣的部分变得越来越困难。 现在&#xff0c;你可以使用搜索功能&#xff0c;快速查找想了解的内容。

金蝶云星空与聚水潭对接集成物料查询连通商品上传(新)(物料主数据同步策略)

金蝶云星空与聚水潭对接集成物料查询连通商品上传&#xff08;新&#xff09;(物料主数据同步策略) 数据源系统:金蝶云星空 金蝶K/3Cloud结合当今先进管理理论和数十万家国内客户最佳应用实践&#xff0c;面向事业部制、多地点、多工厂等运营协同与管控型企业及集团公司&#x…

肖sir__项目环境之全流程__005

一、测试流程&#xff08;h模型&#xff09; 1、需求文档&#xff08;产品&#xff09; 需求文档&#xff08;软件需求规格说明书srs&#xff09; &#xff08;1&#xff09;如何分析需求 a、显示需求&#xff08;主流程、功能&#xff0c;业务&#xff09; b、隐性需求&#x…

java Spring Boot2.7实现一个简单的爬虫功能

首先 我们要在 pom.xml 中注入Jsoup 这是一个简单的java爬虫框架 <dependency><groupId>org.jsoup</groupId><artifactId>jsoup</artifactId><version>1.14.1</version> </dependency>然后这里我们直接用main吧 做简单一点 我…

NSDT孪生场景编辑器系统介绍

一、产品背景 数字孪生的建设流程涉及建模、美术、程序、仿真等多种人才的协同作业&#xff0c;人力要求高&#xff0c;实施成本高&#xff0c;建设周期长。如何让小型团队甚至一个人就可以完成数字孪生的开发&#xff0c;是数字孪生工具链要解决的重要问题。考虑到数字孪生复杂…

Flask框架-2-[单聊]: flask-socketio实现websocket的功能,实现单对单聊天,flask实现单聊功能

一、概述和项目结构 在使用flask-socketio实现单聊时&#xff0c;需要将会话id(sid) 与用户进行绑定&#xff0c;通过emit(事件,消息,tosid) ,就可以把消息单独发送给某个用户了。 flask_websocket |--static |--js |--jquery-3.7.0.min.js |--socket.io_4.3.1.js |--template…

脑电相关临床试验及数据分析01

临床试验设计01–04 作为一个医疗器械公司的开发–>算法–>项目–>产品&#xff0c;还是想在这里记录一下工作。 直接开始吧 临床试验的设计&#xff0c;主要分为20个部分&#xff0c;分别是 封面 一、申办者信息 二、所有临床试验机构和研究者列表 三、临床试验的…

基础算法--位运算

位运算理解&#xff1a; n >> k&#xff1a;代表n右移k位 比如 000011 >> 1 000001 前面会补零&#xff08;所以第几位是从0开始计算&#xff09; n & 1&#xff1a;表示最后一位是否为1 比如&#xff1a;n 3 0011 而 1 0001 则3 & 1 0011 & 000…

本地Docker Registry远程连接,为你带来高效便捷的镜像管理体验!

Linux 本地 Docker Registry本地镜像仓库远程连接 文章目录 Linux 本地 Docker Registry本地镜像仓库远程连接1. 部署Docker Registry2. 本地测试推送镜像3. Linux 安装cpolar4. 配置Docker Registry公网访问地址5. 公网远程推送Docker Registry6. 固定Docker Registry公网地址…

多输入多输出 | MATLAB实现LSSVM最小二乘支持向量机多输入多输出

多输入多输出 | MATLAB实现LSSVM最小二乘支持向量机多输入多输出 目录 多输入多输出 | MATLAB实现LSSVM最小二乘支持向量机多输入多输出预测效果基本介绍程序设计往期精彩参考资料 预测效果 基本介绍 MATLAB实现LSSVM最小二乘支持向量机多输入多输出 1.data为数据集&#xff0c…

Python 判断三位水仙花数

"""判断是否为三位水仙花数知识点&#xff1a;0、水仙花满足条件&#xff1a;(1 ** 3) (5 ** 3) (3 ** 3) 1531、字符串索引&#xff0c;例如&#xff1a;name zhouhua name[0] z2、变量类型转换函数3、双目运算符幂**,例如&#xff1a;3 ** 2 3 * 3 94、…

【Tricks】关于如何防止edge浏览器偷取chrome浏览器的账号

《关于如何防止edge浏览器偷取chrome浏览器的账号》 前段时间edge自动更新了&#xff0c;我并没有太在意界面的问题。但是由于我使用同一个网站平台时&#xff0c;例如b站&#xff0c;甚至是邮箱&#xff0c;edge的账号和chrome的账号会自动同步&#xff0c;这就导致我很难短时…

Linux su sudo命令

1、su命令——切换用户 1.1、切换到root用户(需要密码) su - root 1.2、切换到其他用户&#xff0c;比如jackma&#xff08;无需密码&#xff09; su - jackma 2、sudo命令——给普通用户添加root权限 2.1、用法 切换到root用户&#xff0c;执行visudo命令&#xff0c;会自动…

Leetcode 951. 翻转等价二叉树

文章目录 题目代码&#xff08;9.22 首刷部分看解析&#xff09; 题目 Leetcode 951. 翻转等价二叉树 代码&#xff08;9.22 首刷部分看解析&#xff09; class Solution { public:bool flipEquiv(TreeNode* root1, TreeNode* root2) {if(!root1 && !root2)return tr…

AI也需要透明度?是的,需要

文章目录 什么是AI透明度为什么需要AI透明度AI透明度的弱点如何做好AI透明度推荐阅读 什么是AI透明度 AI透明度指的是人工智能&#xff08;AI&#xff09;系统的工作原理和决策过程能够被理解、解释和追踪的程度。它包括以下几个方面&#xff1a; 可解释性&#xff08;Explai…

安装nvm 切换不同nodejs版本号

1.下载nvm:NVM下载 - NVM中文网 2.卸载node&#xff08;没有安装的可以直接跳过&#xff09; 3.安装 nvm list available 查看可安装的node版本nvm install 12.16.0 安装指定版本node nvm ls 查看已安装的node版本nvm use 16.13.0 切换…