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库对视频中的每一帧进行边缘检测,并将结果保存为新的视频文件。边缘检测是一种图像处理技术,它可以帮助我们识别出图像中不同区域之间的边界。在计算机视觉领域,这项技术有着广泛的应用,比如物体…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...
从零实现富文本编辑器#5-编辑器选区模型的状态结构表达
先前我们总结了浏览器选区模型的交互策略,并且实现了基本的选区操作,还调研了自绘选区的实现。那么相对的,我们还需要设计编辑器的选区表达,也可以称为模型选区。编辑器中应用变更时的操作范围,就是以模型选区为基准来…...
可靠性+灵活性:电力载波技术在楼宇自控中的核心价值
可靠性灵活性:电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中,电力载波技术(PLC)凭借其独特的优势,正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据,无需额外布…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
MMaDA: Multimodal Large Diffusion Language Models
CODE : https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA,它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构…...
页面渲染流程与性能优化
页面渲染流程与性能优化详解(完整版) 一、现代浏览器渲染流程(详细说明) 1. 构建DOM树 浏览器接收到HTML文档后,会逐步解析并构建DOM(Document Object Model)树。具体过程如下: (…...
新能源汽车智慧充电桩管理方案:新能源充电桩散热问题及消防安全监管方案
随着新能源汽车的快速普及,充电桩作为核心配套设施,其安全性与可靠性备受关注。然而,在高温、高负荷运行环境下,充电桩的散热问题与消防安全隐患日益凸显,成为制约行业发展的关键瓶颈。 如何通过智慧化管理手段优化散…...
【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...
从零实现STL哈希容器:unordered_map/unordered_set封装详解
本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说,直接开始吧! 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...
