CopyOnWriteArrayList 使用介绍

目录

    • 一、背景
      • 1.1 线程安全问题复现
      • 1.2 问题跟踪
        • 线程安全隐患一:数组越界
        • 线程安全隐患二:值为null
    • 二、定义
      • 2.1 什么是 CopyOnWriteArrayList?
      • 2.2 CopyOnWriteArrayList 的优点
      • 2.3 CopyOnWriteArrayList 的缺点
    • 三、解决问题

一、背景

1.1 线程安全问题复现

如果我们想把一系列执行结果放到 List 集合中,可能会这样实现:

List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {// 处理业务list.add(i);
}

为了提升执行结果,我们可能会用到多线程:

public static void main(String[] args) throws InterruptedException {ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,1,TimeUnit.MINUTES,new LinkedBlockingQueue<>(10000),r -> new Thread(r, "DemoThread-" + r.hashCode()));List<Integer> list = new ArrayList<>();CountDownLatch countDownLatch = new CountDownLatch(1000);for (int i = 0; i < 1000; i++) {int num = i;executor.execute(() -> {try {// 处理业务Thread.sleep(100L);list.add(num);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {countDownLatch.countDown();}});}countDownLatch.await();list.sort(Integer::compareTo);list.forEach(System.out::println);executor.shutdown();
}

执行结果:

在这里插入图片描述

可以看到莫名其妙抛出了 ArrayIndexOutOfBoundsException 异常,而且结果中也出现了null,虽然我们知道 ArrayList<> 是线程不安全的,但是具体是为什么呢?

1.2 问题跟踪

首先看看 ArrayList 类所拥有的部分属性字段:

public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{/*** 列表元素集合数组* 如果新建ArrayList对象时没有指定大小,那么会将EMPTY_ELEMENTDATA赋值给elementData,* 并在第一次添加元素时,将列表容量设置为DEFAULT_CAPACITY */transient Object[] elementData; /*** 列表大小,elementData中存储的元素个数*/private int size;
}

通过这两个字段我们可以看出,ArrayList 的实现主要就是用了一个 Object[],用来保存所有的元素,以及一个 size 变量用来保存当前数组中已经添加了多少元素。

接着我们看下最重要的 add 操作时的源码:

public boolean add(E e) {/*** 添加一个元素时,做了如下两步操作* 1.判断列表的capacity容量是否足够,是否需要扩容* 2.真正将元素放在列表的元素数组里面*/ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;
}
  • ensureCapacityInternal() 这个方法的详细代码我们可以暂时不看,它的作用就是判断如果将当前的新元素添加到列表后面,列表的 elementData 数组的大小是否满足,如果 size+1 的这个需求长度大于 elementData 数组的长度,那么就要对这个数组进行扩容。

由此看到 add 元素时,实际做了两个大的步骤:

  1. 判断 elementData 数组容量是否满足需求;
  2. 在 elementData 对应的位置上设置值。

针对这两个步骤,就出现了两个导致线程不安全的隐患。

线程安全隐患一:数组越界

在第1步 ensureCapacityInternal(size + 1) 中,如果多个线程进行调用可能会导致 elementData 数组越界,具体逻辑如下:

在这里插入图片描述

  1. 列表大小为9,即:size=9。

在这里插入图片描述

  1. 线程 A 开始进入 add 方法,这时它获取到 size 的值为 9,调用 ensureCapacityInternal 方法进行容量判断。
  2. 线程 B 此时也进入 add 方法,它获取到 size 的值也为 9,也开始调用 ensureCapacityInternal 方法。

在这里插入图片描述

  1. 线程 A 发现需求大小为 10,而 elementData 的大小就为 10,可以容纳。于是它不再扩容,返回。
  2. 线程 B 也发现需求大小为 10,也可以容纳,返回。

在这里插入图片描述

  1. 线程 A 开始进行设置值操作,elementData[size++]=e 操作,此时 size 变为 10。
  2. 线程 B 也开始进行设置值操作,它尝试设置 elementData[10]=e,而 elementData 没有进行过扩容,它的下表最大为9。于是此时会爆出一个数组越界的异常 ArrayIndexOutOfBoundsException
线程安全隐患二:值为null

第二步 elementData[size++]=e 设置值的操作同样会导致线程不安全。从这里可以看出,这部操作也不是一个原子操作,它由如下两步操作构成:

  1. elementData[size] = e;
  2. size = size + 1;

在单线程执行这两行代码时,没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程的值覆盖另一个线程添加的值,具体逻辑如下:

在这里插入图片描述

  1. 列表大小为0,即:size=0。

在这里插入图片描述

  1. 线程 A 开始添加一个元素,值为 A,此时它执行第一条操作,将 A 放在了 elementData 下表为 0 的位置上。

在这里插入图片描述

  1. 接着,线程 B 刚好也要开始添加一个值为 B 的元素,且走到了第一步操作。此时线程 B 获取到 size 的值依然为 0,于是它将 B 也放在了 elementData 下标为 0 的位置上。
  2. 线程 A 开始将 size 的值增加为 1。
  3. 线程 B 开始将 size 的值增加为 2。

这样,线程 A、B 执行完毕后,

  • 理想情况下:size=2,elementData[0]=A,elementData[1]=B。
  • 而实际情况变成了:size=2,elementData[0]=B,elementData[1]=null。

在这里插入图片描述

因为线程 A、B 执行完毕后,size=2,所以下一个线程添加元素时,会从下标为 2 的位置上开始:elementData[2]=C。

那么如何解决 ArrayList 的线程安全问题呢?这时候就需要我们的主角 CopyOnWriteArrayList 登场了。


二、定义

2.1 什么是 CopyOnWriteArrayList?

CopyOnWriteArrayList:是 JDK 在并发包(java.util.concurrent)下的一个类,它是 ArrayList 的一个线程安全的变体。CopyOnWrite 即 读写分离,读时共享,写时复制,通俗的理解是:当我们往一个 List 添加元素的时候,不直接往当前 List 添加,而是先将当前 List 进行 Copy,复制出一个新的 List,然后新的容器里添加元素,添加完元素之后,再将原 List 的引用指向新的 List。 这种设计使得读操作可以在原数组上进行,而不需要加锁,从而大大提高了读操作的并发性。

2.2 CopyOnWriteArrayList 的优点

  1. 线程安全: 由于写操作(修改操作),会导致底层数组的复制,因此可以确保在修改过程中不会被其他现场线程干扰,从而保证了线程安全。
  2. 读操作性能高: 由于读操作不需要加锁,因此多个线程可以同时进行读操作,而不会相互阻塞。这使得在高并发场景下,读操作的性能可以得到更好的保障。
  3. 适用于读多写少的场景: 如果一个列表大部分时间都被用来读取,而只有少部分时间被用来修改,那么使用 CopyOnWriteArrayList 是一个很好的选择。因为它可以确保在读取时不会受到修改操作的影响,从而提供稳定的读取性能。

2.3 CopyOnWriteArrayList 的缺点

  1. 内存占用大: 由于每次修改都会导致底层数组的复制,因此如果列表的大小很大,或者修改操作很频繁,那么就会占用大量的内存空间。这可能会导致频繁的垃圾回收,从而影响系统的整体性能。
  2. 数据一致性问题: 由于读取和修改操作是在不同的数组上进行的,因此如果在读取过程中有其他线程进行了修改操作,那么读取到的数据可能不是最新的。也就是说,CopyOnWriteArrayList 只能保证数据的最终一致性,但无法保证数据的实时一致性。
  3. 写操作性能较低: 由于每次修改都需要复制整个底层数组,因此写操作的性能会相对较低。特别是在列表大小很大或者修改操作很频繁的情况下,这种性能下降会更加明显。
  4. 不支持迭代器修改: CopyOnWriteArrayList 的迭代器不支持对列表的修改操作(如 add、remove 等)。如果在迭代过程中尝试修改列表,会抛出 UnsupportedOperationException 异常。这是因为迭代器在迭代过程中持有的是原始数组的引用,而修改操作会导致底层数组的复制和替换,从而导致迭代器的引用失效。因此,如果需要在迭代过程中修改列表,应该使用其他的数据结构或者采取其他的并发控制措施。

三、解决问题

我们再用 CopyOnWriteArrayList 重新实现最开始我们提到的问题:

public static void main(String[] args) throws InterruptedException {ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,1,TimeUnit.MINUTES,new LinkedBlockingQueue<>(10000),r -> new Thread(r, "DemoThread-" + r.hashCode()));CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();CountDownLatch countDownLatch = new CountDownLatch(1000);for (int i = 0; i < 1000; i++) {int num = i;executor.execute(() -> {try {// 处理业务Thread.sleep(100L);list.add(num);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {countDownLatch.countDown();}});}countDownLatch.await();list.forEach(System.out::println);executor.shutdown();
}

执行结果:

在这里插入图片描述

可以看到,没有报错,没有 null 值,完美~ 🎉

整理完毕,完结撒花~ 🌻





参考地址:

1.为什么说ArrayList是线程不安全的?https://blog.csdn.net/u012859681/article/details/78206494

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

React回顾

一、基础 1、使用babel解析 2、不直接使用jsx&#xff0c;jsx写起来很繁琐 3、jsx语法规则 4、函数式组件的使用 5、函数式组件渲染 6、类组件渲染 7、类组件中事件调用this指向问题 8、类组件不能直接改变状态 9、props接收数据类型限制 类型限制放到类组件内部&#xff0c;用…

C#区域医院云LIS信息管理系统源码 标本管理、两癌筛查、数据分析、试剂管理

目录 ​编辑 区域医院云LIS系统功能亮点&#xff1a; 云LIS系统功能&#xff1a; 一、 基础管理 二、 前处理&#xff08;实验室&#xff09; 三、 标本处理 四、 样本检验 五、 统计报表 六、 质控管理 七、 基本工作流程 区域LIS系统特点&#xff1…

【mysql】1812 - Tablespace is missing for table `job`.`xxl_job_log`.

打开表提示&#xff1a; 1812 - Tablespace is missing for table job.xxl_job_log. 1812-表“job”缺少表空间xxl_job_log。 尝试删除表重建表 DROP TABLE IF EXISTS job.xxl_job_log; 提示&#xff1a; 1051 - Unknown table job.xxl_job_log 1051-未知表“job.xxl_job_lo…

【Java EE初阶二十六】简单的表白墙(二)

2. 后端服务器部分 2.1 服务器分析 2.2 代码编写 2.2.2 前端发起一个ajax请求 2.2.3 服务器读取上述请求,并计算出响应 服务器需要使用 jackson 读取到前端这里的数据,并且进行解析&#xff1a; 代码运行图&#xff1a; 2.2.4 回到前端代码&#xff0c;处理服务器返回的响应…

leetcode 3.反转链表;

1.题目&#xff1a; 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 2.用例&#xff1a; 3.题目解析&#xff1a; &#xff08;1&#xff09;函数头&#xff1a; 要求返回结点&#xff0c;就 ListNode* reverseList(ListNode* head)&…

如何使用Lychee+cpolar搭建本地私人图床并实现远程访问存储图片

文章目录 1.前言2. Lychee网站搭建2.1. Lychee下载和安装2.2 Lychee网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4.公网访问测试5.结语 1.前言 图床作为图片集中存放的服务网站&#xff0c;可以看做是云存储的一部分&#xff0c;既可…

【Go-Zero】测试API查询信息无法返回数据库信息与api、rpc文件编写规范

【Go-Zero】测试API查询信息无法返回数据库信息与api、rpc文件编写规范 大家好 我是寸铁&#x1f44a; 总结了一篇测试API查询信息无法返回数据库信息与api、rpc文件编写规范的文章✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 问题背景 大家好&#xff0c;我是寸铁&#xff01…

计算机网络:IP

引言&#xff1a; IP协议是互联网协议族中的核心协议之一&#xff0c;负责为数据包在网络中传输提供路由寻址。它定义了数据包如何在互联网上从源地址传输到目的地址的规则和流程。IP协议使得各种不同类型的网络设备能够相互通信&#xff0c;实现了全球范围内的信息交换。 目录…

环境分析检测小剂量移液用耐受硝酸盐酸PFA材质吸管特氟龙移液枪枪头

PFA枪头&#xff0c;为移液枪专业定制&#xff0c;广泛用于ICP-MS、ICP-OES等痕量分析以及同位素分析等实验室。地质、电子化学品、半导体分析测试、疾控中心、制药厂、环境检测中心等一些机构少量移液用。 规格参考:0.1-0.2ml、1ml、2ml、5ml、10ml等。 目前部分规格可适配普…

数仓项目6.0(二)数仓

中间的几步意义就在于&#xff0c;缓存中间处理数据样式&#xff0c;避免重复计算浪费算力 分层 ODS&#xff08;Operate Data Store&#xff09; Spark计算过程中&#xff0c;存在shuffle的操作&#xff0c;而shuffle会将计算过程一分为二&#xff0c;前一阶段不执行完&…

深度学习 精选笔记(4)线性神经网络-交叉熵回归与Softmax 回归

学习参考&#xff1a; 动手学深度学习2.0Deep-Learning-with-TensorFlow-bookpytorchlightning ①如有冒犯、请联系侵删。 ②已写完的笔记文章会不定时一直修订修改(删、改、增)&#xff0c;以达到集多方教程的精华于一文的目的。 ③非常推荐上面&#xff08;学习参考&#x…

red hat 6.9版本7.9版本 忘记root密码重置方法

文章目录 1、重置密码具体步骤Oracle Linux 6.9版本&#xff1a;1、开机到此界面按e2、在以下界面选择后按e3、选择相应内核后按e4、在此界面输入 single 回车5、回到内核界面后按b进入引导系统6、在#后输入passwd root修改密码7、修改密码&#xff1a;8、Reboot重启9、使用新密…