Java线程安全集合之COW
概述
java.util.concurrent.CopyOnWriteArrayList写时复制顺序表,一种采用写时复制技术(COW)实现的线程安全的顺序表,可代替java.util.ArrayList用于并发环境中。写时复制,在写入时,会复制顺序表的新副本,在新副本中进行写入操作。这种写入操作非常耗时,但在遍历操作远远多于写入操作的并发多线程场景,却非常高效。
COW
假设有一个共享的数组,对其进行读和写操作,在多线程环境下,要保证其数据安全,就需要对其的并发访问操作进行同步,而对共享数组的并发访问主要有三种,并发读和读,并发写和写,并发读和写。
同步一般都是使用锁来保护这个数组,如果使用一个互斥锁来对这三种并发访问进行同步,那么同一时刻只能由一个线程访问数组(无论是读还是写),这会使线程对数组的访问串行化,是非常低效的,因为对于并发写和写操作来说,固然需要进行互斥访问,但读操作不会修改数据,所以互斥的并发读和读操作不仅仅低效,而且是完全没有必要的,当然为了避免数据不一致问题,并发读和写操作也是需要互斥访问的。为了解决互斥读和读操作的问题,可以使用读写锁,用写锁保证并发写和写的互斥访问,用读锁保证并发读和读操作的共享访问,用读锁和写锁的协作来保证并发写和读的互斥访问。读写锁解决共享读的问题,而写入操作由于会修改数据,因此只能进行互斥访问,而并发读和写操作,由于为了避免数据不一致问题,也需要进行互斥,那么在一些场景下,如果读和写操作之间存在着大量竞争,而读写操作之间又采用互斥同步机制,那么对共享数组的访问就会非常低效。
除互斥同步机制外,还可采用读写分离技术,让读和写操作分别面向不同的实体,这样就不会存在并发问题。写时复制技术就是一种读写分离技术。
写时复制技术是一种共享同步机制,使并发的读和写操作可以同时进行,无需互相等待。采用写时复制技术,只需要一个写锁来对并发写和写操作进行互斥同步。也是空间换时间的实践。
写时复制就是在每次修改共享数组的状态时,需要先复制原数组的副本,然后在副本上进行写入操作,写入完毕后,让共享数组的引用指向新的副本,基本流程如下:复制 - 写入 - 引用变量修改。其中复制操作对于原数组来说只是读取操作,不改变原数组状态,因此完全可以同其他读操作同时执行,而写入操作修改的是原数组副本,此刻共享数组的引用还未指向该副本,因此对其它线程来说是未知的,其它线程如果在此刻需要读取共享数组,那么通过共享数组的引用获取的数组是原数组对象,而原数组状态并未发生任何变化,所以这个阶段也可以同其他读操作同时执行,而最终的赋值操作,因为这一步操作特别快,只是修改变量的值,并且对于引用变量的访问修改,本身都是同步的(是由底层硬件和操作系统控制),这一步对于共享数组本身来说,没有任何影响。
CopyOnWriteArrayList
不同JDK版本里,源码实现有不同。鉴于绝大多数公司(盲猜3个9,99.9%)还是使用JDK8,有必要先看看JDK8源码实现。
JDK8
2个成员变量
final transient ReentrantLock lock = new ReentrantLock(); // 写锁
private transient volatile Object[] array; // 指向数组的引用
/*** 获取数组,非私有,方便CopyOnWriteArraySet类访问*/
final Object[] getArray() {return array;
}
解读:
- lock是一个写锁,用于保证并发写操作的互斥访问,保证同一时刻只有一个线程能够对列表进行写入;
- array是一个数组引用变量,关联数组对象,即CopyOnWriteArrayList的实际存储空间,array关联的数组对象本质上是一个不会再改变的对象,因为一旦array指向这个数组对象,那么CopyOnWriteArrayList就不会再对这个数组对象进行任何形式的修改。
核心方法:
public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;Object[] newElements = Arrays.copyOf(elements, len + 1);newElements[len] = e;setArray(newElements);return true;} finally {lock.unlock();}
}public boolean addAll(Collection<? extends E> c) {Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ? ((CopyOnWriteArrayList<?>)c).getArray() : c.toArray();if (cs.length == 0)return false;final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;if (len == 0 && (c.getClass() == CopyOnWriteArrayList.class || c.getClass() == ArrayList.class))setArray(cs);else {Object[] newElements = Arrays.copyOf(elements, len + cs.length);System.arraycopy(cs, 0, newElements, len, cs.length);setArray(newElements);}return true;} finally {lock.unlock();}
}
解读:add和addAll方法,用于插入新元素。CopyOnWriteArrayList会先加写锁,保证同一时刻只能有一个线程对列表进行修改,通过Arrays.copyOf复制一个副本,同时对数组进行扩容,增加要新增元素的容量,写入新增元素,最后修改array引用变量的值,让array指向新的数组对象。在整个过程,并没有对原数组对象进行任何形式上的修改,所以其他线程可以安全高效的对列表进行任何方式的读操作,而新的数组对象,在被array引用关联之前,都是线程私有的变量,只会被当前线程修改,而不会被其他线程访问,因此是安全的。
CopyOnWriteArrayList的写时复制策略,写入和读取的数组是不同的实体。在写入时会复制一个新的数组副本,而在读取时,都会先获取当前的数组实例,并且使用一个本地引用关联当前的数组实例,如get,forEach等方法,而在相关的读取操作期间,这个数组实例是CopyOnWriteArrayList的一个快照,不会发生任何变化。
获取元素方法:
public E get(int index) {return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {return (E) a[index];
}public void forEach(Consumer<? super E> action) {if (action == null) throw new NullPointerException();Object[] elements = getArray();int len = elements.length;for (int i = 0; i < len; ++i) {@SuppressWarnings("unchecked") E e = (E) elements[i];action.accept(e);}
}public Iterator<E> iterator() {return new COWIterator<E>(getArray(), 0);
}// 遍历iterator时无需同步,不支持remove、set、add等方法
public ListIterator<E> listIterator() {return new COWIterator<E>(getArray(), 0);
}
删除元素方法:
public E remove(int index) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;E oldValue = get(elements, index);int numMoved = len - index - 1;if (numMoved == 0)setArray(Arrays.copyOf(elements, len - 1));else {Object[] newElements = new Object[len - 1];System.arraycopy(elements, 0, newElements, 0, index);System.arraycopy(elements, index + 1, newElements, index, numMoved);setArray(newElements);}return oldValue;} finally {lock.unlock();}
}
所有相关读操作,都是基于快照的读操作。通过CopyOnWriteArrayList迭代器COWIterator的实现源码,对CopyOnWriteArrayList通过迭代器进行迭代操作时,实际上遍历的是创建迭代器的那个时刻的快照,因此在迭代过程中进行修改操作,不会抛出ConcurrentModificationException。
static final class COWIterator<E> implements ListIterator<E> {// 数组快照private final Object[] snapshot;// 调用next时返回的元素索引private int cursor;// 省略私有构造方法public boolean hasNext() {return cursor < snapshot.length;}public boolean hasPrevious() {return cursor > 0;}@SuppressWarnings("unchecked")public E next() {if (! hasNext())throw new NoSuchElementException();return (E) snapshot[cursor++];}@SuppressWarnings("unchecked")public E previous() {if (! hasPrevious())throw new NoSuchElementException();return (E) snapshot[--cursor];}public int nextIndex() {return cursor;}public int previousIndex() {return cursor-1;}@Overridepublic void forEachRemaining(Consumer<? super E> action) {Objects.requireNonNull(action);Object[] elements = snapshot;final int size = elements.length;for (int i = cursor; i < size; i++) {@SuppressWarnings("unchecked") E e = (E) elements[i];action.accept(e);}cursor = size;}
}
COWIterator迭代器不支持任何修改操作,如remove,add,set等方法,都会抛出UnsupportedOperationException,因为CopyOnWriteArrayList使用的是基于快照的读写分离技术,COWIterator本身是一个基于快照的迭代器,而快照是不可变的。
public void remove() {throw new UnsupportedOperationException();
}public void set(E e) {throw new UnsupportedOperationException();
}public void add(E e) {throw new UnsupportedOperationException();
}
缺点
- 内存占用:由于每次写入时都会对数组对象进行复制,复制过程不仅会占用双倍内存,还需要消耗CPU等资源,所以当列表中的元素比较少时,这对内存和GC并没有多大影响,但是当列表保存大量元素时,CopyOnWriteArrayList的底层数组对象有可能会变成一个大对象,这时对CopyOnWriteArrayList每一次修改,都会重新创建一个大对象,并且原来的大对象也需要回收,这都可能会触发GC,特别是大对象会触发Full GC;
JDK11
JDK8之后的下一个LTS版本JDK就是JDK11。发现还是2个成员变量,不过不再使用ReentrantLock而使用synchronized同步锁。
/*** 保护所有变量的锁。(当两者都可以时,我们更喜欢内置监视器而不是 ReentrantLock。)*/
final transient Object lock = new Object();
private transient volatile Object[] array;
再看看add方法:
public boolean add(E e) {synchronized (lock) {Object[] es = getArray();int len = es.length;es = Arrays.copyOf(es, len + 1);es[len] = e;setArray(es);return true;}
}
其他方法也都是使用synchronized同步锁。
适用场景
CopyOnWriteArrayList非常适合读多写少的场景,例如缓存列表或事件监听器集合。创建副本的开销可被大量的读操作所抵消。
其他
CopyOnWriteArraySet
java.util.concurrent.CopyOnWriteArraySet使用装饰器模式利用CopyOnWriteArrayList实现的线程安全的集合类,通过遍历比对的方式来判断集合内是否已经存在该元素。所以如果需要对大量元素进行排重,CopyOnWriteArraySet性能会很差。
private final CopyOnWriteArrayList<E> al;
// 构造函数
public CopyOnWriteArraySet() {al = new CopyOnWriteArrayList<E>();
}
脏读
脏读是指一个线程在读取某个变量时,另一个线程可能正在修改该变量。导致读取到的数据可能是无效或不一致的。
CopyOnWriteArrayList不存在脏读问题:
- 当调用add、set或remove等修改操作时,CopyOnWriteArrayList会创建当前数组的一个新副本,在新副本上执行修改操作,最后用新数组替换旧数组。这个过程是原子性的(通过ReentrantLock或synchronized实现);
- 在读操作中,CopyOnWriteArrayList直接访问当前数组,且不会被写操作阻塞。不会进行加锁,读取操作非常高效,且能够快速返回当前数组的内容。
{@inheritDoc}
看CopyOnWriteArrayList源码时

发现源码里注释是这样:
/*** {@inheritDoc}*/
public int indexOf(Object o) {Object[] es = getArray();return indexOfRange(o, es, 0, es.length);
}
{@inheritDoc}出现在CopyOnWriteArrayList的很多方法。经过分析,想要看原始注释,需要去父类里对应方法找,如List.indexOf()。
参考
相关文章:
Java线程安全集合之COW
概述 java.util.concurrent.CopyOnWriteArrayList写时复制顺序表,一种采用写时复制技术(COW)实现的线程安全的顺序表,可代替java.util.ArrayList用于并发环境中。写时复制,在写入时,会复制顺序表的新副本&…...
智能汽车制造:海康NVR管理平台/工具EasyNVR多品牌NVR管理工具/设备实现无插件视频监控直播方案
一、背景介绍 近年来,随着网络在我国的普及和深化发展,企业的信息化建设不断深入,各行各业都加快了信息网络平台的建设,大多数单位已经或者正在铺设企业内部的计算机局域网。与此同时,网络也成为先进的新兴应用提供了…...
[渗透]前端源码Chrome浏览器修改并运行
文章目录 简述本项目所使用的代码[Fir](https://so.csdn.net/so/search?qFir&spm1001.2101.3001.7020) Cloud 完整项目 原始页面修改源码本地运行前端源码修改页面布局修改请求接口 本项目请求方式 简述 好久之前,就已经看到,_无论什么样的加密&am…...
SAP揭秘者-怎么查看SAP 版本及S4 HANA的版本
文章摘要: 在给客户实施SAP项目或部署SAP服务器及SAP跟外部系统集成时,经常客户或第三方软件公司会问SAP版本或SAP HANA的版本。那么到底怎么来看这个SAP的版本呢?这个问题其实很多SAP模块顾问都不知道怎么看,你可以想象一下&…...
UE4 材质学习笔记13(格斯特纳波)
一.格斯特纳波 要让水面动起来,必须要保证平面有足够的三角面。我们可以在材质里的细节面板打开曲面细分,可以分裂三角面且使之数量更多,选择“扁平曲面细分,其作用是切割我的三角面,然后给我做一大堆三角面出来。 这…...
简述 C# 二维数据集合 List 的创建、遍历、修改、输出
简述 C# 二维数据集合 List 的创建、遍历、修改、输出 1、为什么要使用列表 List2、引入命名空间3、声明一维列表 List4、声明创建一个二维列表 List,数据类型 int5、 简单访问元素6、遍历二维列表,控制台输出7、遍历二维列表,修改数据&#…...
ps2024 一键安装教程 永久使用!
下载后,直接解压打开exe文件就能安装了 下载: https://pan.baidu.com/s/1uDSug00prwRw5igF0N-Xhw?pwd8888 【软件名称】:ps2024 【软件大小】:4.7g 【软件版本】:25.12.0.806 【软件简介】:Photoshop,简称“PS”,是由美国Adobe公司推出…...
ScrollView 真机微信小程序无法隐藏滚动条
问题描述 根据官方文档,使用:show-scrollbar"false",隐藏滚动条无效 解决方法 添加一段样式在 scroll-view 上或者父级节点上下 ::-webkit-scrollbar {width: 0;height: 0;color: transparent;display: none;} eg. .inforDetails_app {p…...
【日志】编辑器开发——修复根据Excel表格数据生成Json文件和配置表代码报错
2024.10.15 又是蕉绿且摆烂的一天,不仅需要克制网瘾,还要努力学习,不然真的会被抛弃啊。但是我还是不想卷,给我的时间大概还有半年,突然好奇半年时间到底能学点什么或者做点什么。 【力扣刷题】 暂无 【数据结构】 …...
C#线性查找算法
前言 线性查找算法是一种简单的查找算法,用于在一个数组或列表中查找一个特定的元素。它从数组的第一个元素开始,逐个检查每个元素,直到找到所需的元素或搜索完整个数组。线性查找的时间复杂度为O(n),其中n是数组中的元素数量。 …...
GPT+Python)近红外光谱数据分析与定性/定量建模技巧
2022年11月30日,可能将成为一个改变人类历史的日子——美国人工智能开发机构OpenAI推出了聊天机器人ChatGPT3.5,将人工智能的发展推向了一个新的高度。2023年4月,更强版本的ChatGPT4.0上线,文本、语音、图像等多模态交互方式使其在…...
Spark动态资源释放机制 详解
Apache Spark 是一个分布式数据处理框架,其动态资源分配(或称为动态资源释放)机制,是为了更高效地利用集群资源,尤其是在执行具有不同工作负载的作业时。Spark 的动态资源释放机制允许它根据作业的需求自动分配和释放集…...
基于径向基神经网络(RBF)的构网型VSG自适应惯量控制MATLAB仿真模型
微❤关注“电气仔推送”获得资料(专享优惠) 模型简介 逆变器虚拟同步发电机控制和核心控制参数就是虚拟惯量与虚拟阻尼,目前的文献中已有众多论文对VSG的虚拟参数展开了研究,但是百分之90都是采用构造函数的方法,使用…...
简单汇编教程9 字符串与字符串指令
目录 字符串的指令 movs 字符串传送 lods, stos使用 cmpsb的使用 SCASB的使用 字符串你很熟悉了,我们定义了无数次了! %macro ANNOUNCE_STRING 2%1 db %2%1_LEN equ $ - %1 %endmacro 当然,我们现在来学习一个比较新的定义方式…...
Taro构建的H5页面路由切换返回上一页存在白屏页面过渡
目录 项目背景:Taro与Hybrid开发问题描述:白屏现象可能的原因包括: 解决方案解决后的效果图 其他优化方案可参考: 项目背景:Taro与Hybrid开发 项目使用Taro框架同时开发微信小程序和H5页面,其中H5页面被嵌…...
【学习笔记】网络设备(华为交换机)基础知识 9 —— 堆叠配置
提示:学习华为交换机堆叠配置,含堆叠的概念、功能、角色、ID和优先级;堆叠的建立过程以及注意事项;包含堆叠的配置命令,以及堆叠的配置案例 一、前期准备 1.已经可以正常访问交换机的命令行接口 Console口本地访问教…...
jeston编译配置cuda加速版opencv
1.源码下载连接 opencv:Releases - OpenCV opencv-contrib: https://github.com/opencv/opencv_contrib 建议不要下最新版本 一般我会下4.5.4 // 4.5.6 // 4.6.0 opencv和opencv-contrib版本要对齐 将下好的opencv和opencv-contrib解压 将opencv-c…...
ApacheShiro反序列化 550 721漏洞
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理个漏洞被称为 Shiro550 是因为在Apache Shiro的GitHub问题跟踪器中,该漏洞最初被标记为第550个问题,721漏洞名称也是由此而来 Shiro-550 CVE-2016-4437 Shiro反序列化Docker复现 …...
Github + 自定义域名搭建个人静态站点
Github 自定义域名搭建个人静态站点 使用 Github 部署一个自己的免费站点给你的站点添加上自定义域名 本文基于腾讯云基于二级域名, 作用于 Github 实现自定义域名站点 使用 Github 部署一个自己的免费站点 首先你得有一个 Github 账号, 没有就去注册一个,网上有教程,本文跳…...
使用OpenCV进行视频边缘检测:案例Python版江南style
1. 引言 本文将演示如何使用OpenCV库对视频中的每一帧进行边缘检测,并将结果保存为新的视频文件。边缘检测是一种图像处理技术,它可以帮助我们识别出图像中不同区域之间的边界。在计算机视觉领域,这项技术有着广泛的应用,比如物体…...
linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...
DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径
目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
Spring AI与Spring Modulith核心技术解析
Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
