集合系列(十五) -CopyOnWriteArrayList详解

一、摘要

在介绍 CopyOnWriteArrayList 之前,我们一起先来看看如下方法执行结果,代码内容如下:

public static void main(String[] args) {List<String> list = new ArrayList<String>();list.add("1");list.add("2");list.add("1");System.out.println("原始list元素:"+ list.toString());//通过对象移除等于内容为1的元素for (String item : list) {if("1".equals(item)) {list.remove(item);}}System.out.println("通过对象移除后的list元素:"+ list.toString());
}

执行结果内容如下:

原始list元素:[1, 2, 1]
Exception in thread "main" java.util.ConcurrentModificationExceptionat java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)at java.util.ArrayList$Itr.next(ArrayList.java:859)at com.example.container.a.TestList.main(TestList.java:16)

很遗憾,结果并没有达到我们想要的预期效果,执行之后直接报错!抛ConcurrentModificationException异常!

为啥会抛这个异常呢?

我们一起来看看,foreach 写法实际上是对List.iterator() 迭代器的一种简写,因此我们可以从分析List.iterator() 迭代器进行入手,看看为啥会抛这个异常。

ArrayList类中的Iterator迭代器实现,源码内容:

通过代码我们发现 ItrArrayList 中定义的一个私有内部类,每次调用nextremove方法时,都会调用checkForComodification方法,源码如下:

/**修改次数检查*/
final void checkForComodification() {//检查List中的修改次数是否与迭代器类中的修改次数相等if (modCount != expectedModCount)throw new ConcurrentModificationException();
}

checkForComodification方法,实际上是用来检查List中的修改次数modCount是否与迭代器类中的修改次数expectedModCount相等,如果不相等,就会抛出ConcurrentModificationException异常!

那么问题基本上已经清晰了,上面的运行结果之所以会抛出这个异常,就是因为List中的修改次数modCount与迭代器类中的修改次数expectedModCount不相同造成的!

阅读过集合源码的朋友,可能想起Vector这个类,它不是 JDK 中 ArrayList 线程安全的一个版本么?

好的,为了眼见为实,我们把ArrayList换成Vector来测试一下,代码如下:

public static void main(String[] args) {Vector<String> list = new Vector<String>();//模拟10个线程向list中添加内容,并且读取内容for (int i = 0; i < 5; i++) {final int j = i;new Thread(new Runnable() {@Overridepublic void run() {//添加内容list.add(j + "-j");//读取内容for (String str : list) {System.out.println("内容:" + str);}}}).start();}
}

执行程序,运行结果如下:

还是一样的结果,抛异常了Vector虽然线程安全,只不过是加了synchronized关键字,但是迭代问题完全没有解决!

继续回到本文要介绍的 CopyOnWriteArrayList 类,我们把上面的例子,换成CopyOnWriteArrayList类来试试,源码内容如下:

public static void main(String[] args) {//将ArrayList换成CopyOnWriteArrayListCopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();list.add("1");list.add("2");list.add("1");System.out.println("原始list元素:"+ list.toString());//通过对象移除等于11的元素for (String item : list) {if("1".equals(item)) {list.remove(item);}}System.out.println("通过对象移除后的list元素:"+ list.toString());
}

执行结果如下:

原始list元素:[1, 2, 1]
通过对象移除后的list元素:[2]

呃呵,执行成功了,没有报错!是不是很神奇~~

当然,类似上面这样的例子有很多,比如写10个线程向list中添加元素读取内容,也会抛出上面那个异常,操作如下:

public static void main(String[] args) {final List<String> list = new ArrayList<>();//模拟10个线程向list中添加内容,并且读取内容for (int i = 0; i < 10; i++) {final int j = i;new Thread(new Runnable() {@Overridepublic void run() {//添加内容list.add(j + "-j");//读取内容for (String str : list) {System.out.println("内容:" + str);}}}).start();}
}

类似的操作例子就非常多了,这里就不一一举例了。

CopyOnWriteArrayList 实际上是 ArrayList 一个线程安全的操作类!

从它的名字可以看出,CopyOnWrite 是在写入的时候,不修改原内容,而是将原来的内容复制一份到新的数组,然后向新数组写完数据之后,再移动内存指针,将目标指向最新的位置。

二、简介

从 JDK1.5 开始 Java 并发包里提供了两个使用CopyOnWrite 机制实现的并发容器,分别是CopyOnWriteArrayListCopyOnWriteArraySet

从名字上看,CopyOnWriteArrayList主要针对动态数组,一个线程安全版本的 ArrayList !

CopyOnWriteArraySet主要针对集,CopyOnWriteArraySet可以理解为HashSet线程安全的操作类,我们都知道HashSet基于散列表HashMap实现,但是CopyOnWriteArraySet并不是基于散列表实现,而是基于CopyOnWriteArrayList动态数组实现!

关于这一点,我们可以从它的源码中得出结论,部分源码内容:

从源码上可以看出,CopyOnWriteArraySet默认初始化的时候,实例化了CopyOnWriteArrayList类,CopyOnWriteArraySet的大部分方法,例如addremove等方法都基于CopyOnWriteArraySet实现!

两者最大的不同点是,CopyOnWriteArrayList可以允许元素重复,而CopyOnWriteArraySet不允许有重复的元素!

好了,继续来 BB 本文要介绍的CopyOnWriteArrayList类~~

打开CopyOnWriteArrayList类的源码,内容如下:

可以看到 CopyOnWriteArrayList 的存储元素的数组array变量,使用了volatile关键字保证的多线程下数据可见行;同时,使用了ReentrantLock可重入锁对象,保证线程操作安全。

在初始化阶段,CopyOnWriteArrayList默认给数组初始化了一个对象,当然,初始化方法还有很多,比如如下我们经常会用到的一个初始化方法,源码内容如下:

这个方法,表示如果我们传入的是一个 ArrayList数组对象,会将对象内容复制一份到新的数组中,然后初始化进去,操作如下:

List<String> list = new ArrayList<>();
...
//CopyOnWriteArrayList将list内容复制出来,并创建一个新的数组
CopyOnWriteArrayList<String> copyList = new CopyOnWriteArrayList<>(list);

CopyOnWriteArrayList是对原数组内容进行复制再写入,那么是不是也存在多线程下操作也会发生冲突呢?

下面我们再一起来看看它的方法实现!

三、常用方法

3.1、添加元素

add()方法是CopyOnWriteArrayList的添加元素的入口!

CopyOnWriteArrayList之所以能保证多线程下安全操作, add()方法功不可没,源码如下:

操作步骤如下:

  • 1、获得对象锁;
  • 2、获取数组内容;
  • 3、将原数组内容复制到新数组;
  • 4、写入数据;
  • 5、将array数组变量地址指向新数组;
  • 6、释放对象锁;

在 Java 中,独占锁方面,有2种方式可以保证线程操作安全,一种是使用虚拟机提供的synchronized 来保证并发安全,另一种是使用JUC包下的ReentrantLock可重入锁来保证线程操作安全。

CopyOnWriteArrayList使用了ReentrantLock这种可重入锁,保证了线程操作安全,同时数组变量array使用volatile保证多线程下数据的可见行!

其他的,还有指定下标进行添加的方法,如add(int index, E element),操作类似,先找到需要添加的位置,如果是中间位置,则以添加位置为分界点,分两次进行复制,最后写入数据!

3.2、移除元素

remove()方法是CopyOnWriteArrayList的移除元素的入口!

源码如下:

操作类似添加方法,步骤如下:

  • 1、获得对象锁;
  • 2、获取数组内容;
  • 3、判断移除的元素是否为数组最后的元素,如果是最后的元素,直接将旧元素内容复制到新数组,并重新设置array值;
  • 4、如果是中间元素,以index为分界点,分两节复制;
  • 5、将array数组变量地址指向新数组;
  • 6、释放对象锁;

当然,移除的方法还有基于对象的remove(Object o),原理也是一样的,先找到元素的下标,然后执行移除操作。

3.3、查询元素

get()方法是CopyOnWriteArrayList的查询元素的入口!

源码如下:

public E get(int index) {//获取数组内容,通过下标直接获取return get(getArray(), index);
}

查询因为不涉及到数据操作,所以无需使用锁进行处理!

3.4、遍历元素

上文中我们介绍到,基本都是在遍历元素的时候因为修改次数与迭代器中的修改次数不一致,导致检查的时候抛异常,我们一起来看看CopyOnWriteArrayList迭代器实现。

打开源码,可以得出CopyOnWriteArrayList返回的迭代器是COWIterator,源码如下:

public Iterator<E> iterator() {return new COWIterator<E>(getArray(), 0);
}

打开COWIterator类,其实它是CopyOnWriteArrayList的一个静态内部类,源码如下:

可以看出,在使用迭代器的时候,遍历的元素都来自于上面的getArray()方法传入的对象数组,也就是传递进来的 array 数组!

由此可见,CopyOnWriteArrayList 在使用迭代器遍历的时候,操作的都是原数组,没有像上面那样进行修改次数判断,所以不会抛异常!

当然,从源码上也可以得出,使用CopyOnWriteArrayList的迭代器进行遍历元素的时候,不能调用remove()方法移除元素,因为不支持此操作!

如果想要移除元素,只能使用CopyOnWriteArrayList提供的remove()方法,而不是迭代器的remove()方法,这个需要注意一下!

四、总结

CopyOnWriteArrayList是一个典型的读写分离的动态数组操作类!

在写入数据的时候,将旧数组内容复制一份出来,然后向新的数组写入数据,最后将新的数组内存地址返回给数组变量;移除操作也类似,只是方式是移除元素而不是添加元素;而查询方法,因为不涉及线程操作,所以并没有加锁出来!

因为CopyOnWriteArrayList读取内容没有加锁,在写入数据的时候同时也可以进行读取数据操作,因此性能得到很大的提升,但是也有缺陷,对于边读边写的情况,不一定能实时的读到最新的数据,比如如下操作:

public static void main(String[] args) throws InterruptedException {final CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();list.add("a");list.add("b");for (int i = 0; i < 5; i++) {final int j =i;new Thread(new Runnable() {@Overridepublic void run() {//写入数据list.add("i-" + j);//读取数据for (String str : list) {System.out.println("线程-" + Thread.currentThread().getName() + ",读取内容:" + str);}}}).start();}
}

新建5个线程向list中添加元素,执行结果如下:

可以看到,5个线程的读取内容有差异!

因此CopyOnWriteArrayList很适合读多写少的应用场景!

五、参考

1、JDK1.7&JDK1.8 源码

2、掘金 - 拥抱心中的梦想 - 说一说Java中的CopyOnWriteArrayList

六、写到最后

最近无意间获得一份阿里大佬写的技术笔记,内容涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL 等技术知识。需要的小伙伴可以点击如下链接获取,资源地址:技术资料笔记。

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!

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

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

相关文章

[BT]BUUCTF刷题第8天(3.26)

第8天 Web [CISCN2019 华北赛区 Day2 Web1]Hack World 题目明确提示flag在flag表里的flag列&#xff0c;这里先尝试1 返回&#xff1a;你好&#xff0c;glzjin想要一个女朋友。 再尝试1&#xff0c;返回bool(false) 到这里就感觉是布尔盲注的题目类型了&#xff08;虽然我没…

EtherCAT转RS232网关在风电领域的应用

开疆智能EtherCAT转RS232网关在风电领域的应用主要体现在以下几个方面&#xff1a; 1.数据采集与传输&#xff1a;在风力发电设备中&#xff0c;传感器和执行器的数据采集和传输至关重要。EtherCAT转RS232网关可以将风力发电设备中的RS232通信协议转换为EtherCAT协议&#xff0…

港澳青年看祖国—千名青年创业家内地暨江港青年交流活动在江举行

为聚焦“一点两地”全新定位&#xff0c;助力纵深推进新阶段粤港澳大湾区建设&#xff0c;3月22日&#xff0c;江门市委统战部、团市委、市青联联合香港深水埗区青年发展及公民教育委员会、愿景基金会、香港青年创业家总商会举办千名青年创业家内地行暨江港青年交流活动&#x…

【小黑送书—第十四期】>>重磅升级——《Excel函数与公式应用大全》(文末送书)

今天给大家带来AI时代系列书籍&#xff1a;《Excel 2019函数与公式应用大全》全新升级版&#xff0c;Excel Home多位微软全球MVP专家打造&#xff0c;精选Excel Home海量案例&#xff0c;披露Excel专家多年研究成果&#xff0c;让你分分钟搞定海量数据运算&#xff01; 由北京…

Redis 教程系列之Redis PHP 使用 Redis(十二)

PHP 使用 Redis 安装 开始在 PHP 中使用 Redis 前&#xff0c; 我们需要确保已经安装了 redis 服务及 PHP redis 驱动&#xff0c;且你的机器上能正常使用 PHP。 接下来让我们安装 PHP redis 驱动&#xff1a;下载地址为:https://github.com/phpredis/phpredis/releases。 P…

fs.1.10 ON CENTOS7 docker镜像制作

概述 freeswitch是一款简单好用的VOIP开源软交换平台。 centos7 docker上编译安装fs1.10版本的流程记录。 环境 docker engine&#xff1a;Version 24.0.6 centos docker&#xff1a;7 freeswitch&#xff1a;v1.10.7 手动模式 centos准备 docker hub拉取centos镜像。…

3.26学习总结java初步实现学生管理系统

(该项目通过视频讲解过程中完成,其中将一些操作进行了修改和完善,其目的是为了巩固前面学习java的一些用法,熟悉写项目的过程) 一.项目要求 学生类: 属性:id、姓名、年龄、家庭住址 添加功能: 键盘录入每一个学生信息并添加&#xff0c;需要满足以下要求: ID唯一 删除功能…

VMware扩容硬盘

最近研究Oracle的备份导入导出功能&#xff0c;但是因为磁盘容量不够导致表空间的扩容没办法&#xff0c;从而没办法导入数据库的dmp文件。得想办法先扩容磁盘容量。话不多说上截图操作。 操作环境&#xff1a;VMware10 , Centos 6.9 VMware扩容硬盘步骤 一、关闭虚拟机&…

v4l2采集视频

Video4Linux2&#xff08;v4l2&#xff09;是用于Linux系统的视频设备驱动框架&#xff0c;它允许用户空间应用程序直接与视频设备&#xff08;如摄像头、视频采集卡等&#xff09;进行交互。 linux系统下一切皆文件&#xff0c;对视频设备的操作就像对文件的操作一样&#xff…

蓝桥杯刷题-子串简写

子串简写 代码 kint(input()) s,c1,c2input().split() pre[0]*len(s) ans0 for i in range(len(s)):pre[i]pre[i-1]if c1s[i]:pre[i]1elif c2s[i] and i1-k>0:anspre[i-k1] print(ans)

【MySQL】4.MySQL日志管理与数据库的备份和恢复

备份的目的只要是为了灾难恢复&#xff0c;备份还可以测试应用&#xff0c;回滚数据&#xff0c;修改和查询历史数据&#xff0c;审计等 日志在备份、恢复中起着重要作用 一、数据库备份的重要性 在生产环境中&#xff0c;数据的安全性至关重要 任何数据丢失都可能产生严重的…

苍穹外卖项目-01(开发流程,介绍,开发环境搭建,nginx反向代理,Swagger)

目录 一、软件开发整体介绍 1. 软件开发流程 1 第1阶段: 需求分析 2 第2阶段: 设计 3 第3阶段: 编码 4 第4阶段: 测试 5 第5阶段: 上线运维 2. 角色分工 3. 软件环境 1 开发环境(development) 2 测试环境(testing) 3 生产环境(production) 二、苍穹外卖项目介绍 …

第二证券|股票限售解禁是怎么回事?

限售股是指那些卖出受到限制的股票&#xff0c;其受到了不少投资者的重视。关于股票限售解禁是怎么回事&#xff0c;第二证券下面就为大家详细介绍一下。 股票限售解禁是指一些本来不能在二级商场上自在买卖的股票&#xff0c;当满足必定的条件后能够在二级商场上自在买卖。一…

如何用交换机组建LAN?

什么是LAN&#xff1f; 局域网&#xff08;LAN&#xff09;是连接有限区域&#xff08;例如办公楼、学校或家庭&#xff09;内设备的网络。它允许用户共享资源&#xff0c;包括数据、打印机和互联网访问。LAN连接设备以促进用户之间的协作和传输信息&#xff0c;例如计算机、打…

YOLOv9有效改进专栏汇总|未来更新卷积、主干、检测头注意力机制、特征融合方式等创新![2024/3/23]

​ 专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;助力高效涨点&#xff01;&#xff01;&#xff01; 专栏介绍 YOLOv9作为最新的YOLO系列模型&#xff0c;对于做目标检测的同学是必不可少的。本专栏将针对2024年最新推出的YOLOv9检测模型&#xff0…

【C语言】Infiniband驱动mlx4_reset

一、注释 这个 mlx4_reset 函数负责重置 Mellanox 设备。它保存了设备的 PCI 头信息&#xff0c;然后重置了设备&#xff0c;之后还原保存的 PCI 头信息。请注意&#xff0c;该函数是用英文注释的&#xff0c;下面提供中文注释的版本。以下是该函数的流程&#xff1a; 1. 为保…

基于51单片机的鸡蛋孵化环境监测警报系统Proteus仿真

地址&#xff1a;https://pan.baidu.com/s/1-OTZcuPHiZwdfd5KCaG7NA 提取码&#xff1a;1234 仿真图&#xff1a; 1、使用ADC0808测量一路模拟量&#xff08;可以表示温度、湿度、烟雾等等&#xff09; 2、如果测量值低于阀值&#xff0c;启动继电器&#xff1b;高于阀值&…

STM32之HAL开发——RCC外设CubeMX配置时钟

RCC外设介绍 RCC是Reset and Clock Control (复位和时钟控制)的缩写&#xff0c;它是STM32内部的一个重要外设&#xff0c;负责管理各种时钟源和时钟分频&#xff0c;以及为各个外设提供时钟使能。RCC模块可以通过寄存器操作或者库函数来配置。 RCC是复位和时钟控制模块&#…

Motorbike Physics Tool

此软件包适用于任何想要使用他们拥有的摩托车和玩家模型,并快速将其变成可驾驶摩托车而无需编写一行代码的人。 包说明 在这个工具中,它允许您使用您拥有的任何自行车模型,并将其变成可驾驶的摩托车,而无需编写任何代码。对于那些想在游戏中拥有自行车但又不想学习如何制作…

Go第三方框架--gin框架(二)

4. gin框架源码–Engine引擎和压缩前缀树的建立 讲了这么多 到标题4才开始介绍源码&#xff0c;主要原因还是想先在头脑中构建起 一个大体的框架 然后再填肉 这样不容易得脑血栓。标题四主要涉及标题2.3的步骤一 也就是 标题2.3中的 粗线框中的内容 4.1 Engine 引擎的建立 见…