CopyOnWriteArrayList部分源码分析

2023-11-04

CopyOnWriteArrayList部分源码分析

​ 我们都知道ArrayList是基于数组实现的可动态扩容的集合,但是他实际上也是线程不安全的,而在JUC(java.util.concurrent)下有个线程安全的数组集合,就是CopyOnWriteArrayList,我们将从他的源码开始分析这个是怎么保证线程安全的。

构造函数

​ 在看构造函数之前,我们能看到在COWAL(简称)的类中定义了这么两个参数和两个final的方法

在这里插入图片描述

// 可以看到这里它定义了一个锁
// transient:由于这个类实现了序列化的接口,如果不想该属性被序列化,就可以加上这个关键字
final transient ReentrantLock lock = new ReentrantLock();

// 可以看到这个类仍然是基于数组实现的,并且这个数据线程间是可见的(volatile),在源码中也
// 标明这个数组只能通过getArray()/setArray()获取
private transient volatile Object[] array;

// 获取这个array
final Object[] getArray() {
    return array;
}

// 对array进行赋值
final void setArray(Object[] a) {
    array = a;
}

volatile

​ 既然看到了有volatile这个关键字,就顺便想说一下这个关键字做了什么,如果已经了解了的话可以直接跳过。

它确保了语义上对变量的读、写操作顺序被观察到

  • 对volatile变量的读、写不会被重排到对它后续的读写之后(阻止指令重排)
  • 保证写入的值可以马上同步到CPU缓存之中(写入后要求CPU马上刷新缓存)
  • 保证读取到最新版本的数据(读L3、主存等,甚至使用内存屏障)

如果逻辑上,变量的写在读之前发生,那么确保观察到的结果,写也在读之前发生。

  • 即happens-before关系
    • 如果事件A发生在事件B之前,那么观察到的结果也应如此
    • 时间关系的一致性
  • 确保可见性、有序性

我们继续他的几个构造函数的分析

// 无参构造方法就只是创建了个空的数组
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
// 这里传入的是一个集合,该集合类型为创建COWAL时的类型,就相当于addAll操作,将该集合中的
// 所有元素按照迭代器迭代出来的顺序加入到当前的数组中
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    // 判断传入的集合是否就是COWAL的类型,如果是就直接取里面的数组
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        // 不是的话先取出其中的元素数组
        elements = c.toArray();
        // 如果这个集合的类型不是ArrayList的类型,则将其复制并赋值到局部参数elements中
        if (c.getClass() != ArrayList.class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    // 将局部变量设置到当前COWAL对象的属性array中
    setArray(elements);
}
// 创建一个包含给定数组的副本列表,其实也就是将其复制并赋值到该对象的array中
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

接着我们先看看比较核心的add方法,看看是怎么保证线程安全的

// 将指定元素添加到列表的末尾
public boolean add(E e) {
    // 先对其进行加锁操作,也即当其他线程对该对象进行添加元素的操作时都要进行获取这个锁,获取不到就进入等待
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 先取出当前的数组
        Object[] elements = getArray();
        int len = elements.length;
        // 可以看到此时是将原数组进行了复制,且将长度+1,并没有对当前数组直接进行操作
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 在新数组尾部加入待添加的元素
        newElements[len] = e;
        // 将新数组赋值给当前对象的array中
        setArray(newElements);
        // 返回操作成功
        return true;
    } finally {
        // 释放锁资源
        lock.unlock();
    }
}

接下来看看在指定位置加入元素是怎么操作的

// 在此列表中的指定位置插入指定元素。将当前位于该位置的元素(如果有)和任何后续元素向右移动(将其索引加一)。
public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    // 同样的先进行上锁
    lock.lock();
    try {
        // 获取当前的数组
        Object[] elements = getArray();
        int len = elements.length;
        // 对传入的index判断合法性(不允许小于0也不允许超过当前数组的大小)
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        // 仍是创建新数组
        Object[] newElements;
        // 判断当前的插入位置是否为数组尾部
        int numMoved = len - index;
        if (numMoved == 0)
            // 如果是的话直接将原数组复制到新建的数组中,并且长度 + 1
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            newElements = new Object[len + 1];
            // 否则先将元素截止到index(范围为[0,index))复制到新数组中
            System.arraycopy(elements, 0, newElements, 0, index);
            // 再将剩余的复制到index + 1开始到数组长度的位置上,实际上就是将索引向后推1位
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
       	// 空出的index存放要存放的指定元素
        newElements[index] = element;
        // 将该数组赋值回该对象的array中
        setArray(newElements);
    } finally {
        // 释放锁资源
        lock.unlock();
    }
}

​ 从上面的操作其实我们不难推测出它对删除的实际写法,无非也就是继续上锁,然后获取到当前数组,判断删除的元素的合法性以及是否为最后一位,是就将数组复制一份并将长度 - 1,然后赋值回当前的数组,不然就创建新的长度为原长度 - 1的数组并将数组从index前后拆分开,复制到新数组并剔除掉下标为index的元素,然后赋值回当前对象的array中,最后释放锁资源。

​ 可以总结出,其实对于它的写操作,总是先进行上锁,然后复制出一个新数组对其操作并将这个数组赋值回原数组,由于在读操作是不会进行上锁的,也不需要去获取锁资源,所以添加了volatile关键字使得对象属性中的array的写操作最终被读操作可见,防止出现问题。

​ 而为什么要使用写时复制呢,其实我们可以在ArrayList中看到如果在多线程并发对一个ArrayList对象同时进行读写的时候,会报错,而从源码中我们能看到读操作是没有进行加锁的,如果同样加了锁,那么效率和性能就会大大降低,而如果在并发环境下,写操作会因为锁而阻塞,但读不会,如果此时读写操作的都是同一个对象,仍是会导致不可预估的错误,所以进行了写时复制的操作,写的时候操作是对读操作透明的,但是因为volatile关键字,最终在写回数组时会保证结果对读操作是可见的,这三种操作(lock、volatile、写时复制(copyOnWrite))结合,非常精妙。

​ 以上是我个人对CopyOnWriteArrayList源码分析的个人理解,如有错误请多多指出,大家一起努力~

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

CopyOnWriteArrayList部分源码分析 的相关文章

  • 从 java sdk 向对等方发送提案时出现访问被拒绝错误

    我正在尝试使用以下代码查询区块链并收到访问被拒绝错误 我也遇到同样的错误sendTransactionProposal方法也是如此 UserContext adminUserContext RegisterEnrollUser regist
  • JPA 中的复合键

    我想创建一个具有自动生成的主键的实体 而且还有一个由其他两个字段组成的唯一复合键 我如何在 JPA 中执行此操作 我想这样做是因为主键应该用作另一个表中的外键 并且使其复合并不好 在下面的代码片段中 我需要命令和模型是唯一的 pk当然是主键
  • 如何将 javax.persistence.Column 定义为 Unsigned TINYINT?

    我正在基于 MySQL 数据库中的现有表创建 Java 持久性实体 Bean 使用 NetBeans IDE 8 0 1 我在这个表中遇到了一个字段 其类型为 无符号 TINYINT 3 我发现可以执行以下操作将列的类型定义为 unsign
  • Spring Security 自定义过滤器

    我想自定义 Spring security 3 0 5 并将登录 URL 更改为 login 而不是 j spring security check 我需要做的是允许登录 目录并保护 admin report html 页面 首先 我使用教
  • 使用 Ant 将非代码资源添加到 jar 文件

    我正在将 java 应用程序打包成 jar 文件 我正在使用 ant 和 eclipse 我实际上需要在 jar 中直接在根文件夹下包含几个单独的非代码文件 xml 和 txt 文件 而不是与代码位于同一位置 我正在尝试使用includes
  • 打印星号的 ASCII 菱形

    我的程序打印出这样的钻石 但只有当参数或菱形的每一面为4 例如如果我输入6 底部三角形的间距是错误的 我一直在试图找出答案 当参数改变时 底部的三角形不会改变 只有顶部的三角形会改变 它只适用于输入4 public static void
  • Java:在 eclipse 中导出到 .jar 文件

    我正在尝试将 Eclipse 中的程序导出到 jar 文件 在我的项目中 我添加了一些图片和 PDF s 当我导出到 jar 文件时 似乎只有main已编译并导出 我的意愿是如果可能的话将所有内容导出到 jar 文件 因为这样我想将其转换为
  • Spring Data JPA 选择不同

    我有一个情况 我需要建立一个select distinct a address from Person a 其中地址是 Person 内的地址实体 类型的查询 我正在使用规范动态构建我的 where 子句并使用findAll Specifi
  • 是否可以使用 Flying Saucer (XHTML-Renderer) 将 css 解析为类路径资源?

    我正在尝试将资源打包到 jar 中 但我无法让 Flying Saucer 在类路径上找到 css 我无法轻松构建 URL 来无缝解决此问题 https stackoverflow com questions 861500 url to l
  • 需要使用 joda 进行灵活的日期时间转换

    我想使用 joda 解析电子邮件中的日期时间字符串 不幸的是我得到了各种不同的格式 例如 Wed 19 Jan 2011 12 52 31 0600 Wed 19 Jan 2011 10 15 34 0800 PST Wed 19 Jan
  • 在另一个模块中使用自定义 gradle 插件模块

    我正在开发一个自定义插件 我希望能够在稍后阶段将其部署到存储库 因此我为其创建了一个独立的模块 在对其进行任何正式的 TDD 之前 我想手动进行某些探索性测试 因此 我创建了一个使用给定插件的演示模块 到目前为止 我发现执行此操作的唯一方法
  • 了解joda时间PeriodFormatter

    我以为我明白了 但显然我不明白 你能帮我通过这些单元测试吗 Test public void second assertEquals 00 00 01 OurDateTimeFormatter format 1000 Test public
  • 读取电子邮件的文本文件转换为 Javamail MimeMessage

    我有一个电子邮件原始来源的文本文件 直接从 gmail 复制 如果您单击 查看原始文件 您就会看到它 我想读入该文件并将其转换为 MimeMessage 如果您好奇为什么 我设置了 JavaMaildir 并且需要用电子邮件填充它的收件箱以
  • 在 SWT/JFace RCP 应用程序中填充巨大的表

    您将如何在 SWT 表中显示大量行 巨大是指超过 20K 行 20 列的东西 不要问我为什么需要展示那么多数据 这不是重点 关键是如何让它尽可能快地工作 这样最终用户就不会厌倦等待 每行显示某个对象的实例 列是其属性 一些 我想使用 JFa
  • Docker 和 Eureka 与 Spring Boot 无法注册客户端

    我有一个使用 Spring Boot Docker Compose Eureka 的非常简单的演示 我的服务器在端口 8671 上运行 具有以下应用程序属性 server port 8761 eureka instance prefer i
  • Java Swing:需要一个高质量的带有复选框的开发 JTree

    我一直在寻找一个 Tree 实现 其中包含复选框 其中 当您选择一个节点时 树中的所有后继节点都会被自动选择 当您取消选择一个节点时 树中其所有后继节点都会自动取消选择 当已经选择了父节点 并且从其后继之一中删除了选择时 节点颜色将发生变化
  • 如何重新启动死线程? [复制]

    这个问题在这里已经有答案了 有哪些不同的可能性可以带来死线程回到可运行状态 如果您查看线程生命周期图像 就会发现一旦线程终止 您就无法返回到新位置 So 没有办法将死线程恢复到可运行状态 相反 您应该创建一个新的 Thread 实例
  • Java中HashMap和ArrayList的区别?

    在爪哇 ArrayList and HashMap被用作集合 但我不明白我们应该在哪些情况下使用ArrayList以及使用时间HashMap 他们两者之间的主要区别是什么 您具体询问的是 ArrayList 和 HashMap 但我认为要完
  • 在浏览器刷新中刷新检票面板

    我正在开发一个付费角色系统 一旦用户刷新浏览器 我就需要刷新该页面中可用的统计信息 统计信息应该从数据库中获取并显示 但现在它不能正常工作 因为在页面刷新中 java代码不会被调用 而是使用以前的数据加载缓存的页面 我尝试添加以下代码来修复
  • 洪水填充优化:尝试使用队列

    我正在尝试创建一种填充方法 该方法采用用户指定的初始坐标 检查字符 然后根据需要更改它 这样做之后 它会检查相邻的方块并重复该过程 经过一番研究 我遇到了洪水填充算法并尝试了该算法 它可以工作 但无法满足我对 250 x 250 个字符的数

随机推荐

  • goland面试题第八天

    第八天 1 关于init函数 下面说法正确的是 A 一个包中 可以包含多个 init 函数 B 程序编译时 先执行依赖包的 init 函数 再执行 main 包内的 init 函数 C main 包中 不能有 init 函数 D init
  • conda和pip都可以安装Python包,那么它们有哪些区别呢?

    文章目录 conda和pip都可以安装Python包 那么二者有什么区别呢 如果想单独了解conda可以查看这篇文章 conda详细的使用教程 不仅能够管理Python包 还能管理虚拟环境 了解pip可以查看这篇文章 Python包管理工具
  • lambda no instance(s) of type variable(s) T exist so that void conforms to R

    代码如下出现标题报错 List
  • 初学TypeScript

    TypeScript介绍 开发环境搭建 常用数据类型 1 什么是TypeScript TypeScript 是JavaScript的一个超集 支持ECMAScript 6标准 Typescript由微软开发的自由和开源的编程语言 TypeS
  • 合作搜索优化算法(Matlab代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 Matlab代码实现 4 参考文献 1 概述 合作搜索算法 CSA 在问题空间中随机生成
  • flutter-使用第三方库,编译和运行版本不一致问题 2

    问题 接着上一个问题 如果是继续有问题 你讲看到这篇文章 新的问题又出现啦 如 Command xx xx develop android flutter app android gradlew app properties Finishe
  • Anaconda切换python版本

    比如 我的 已经安装好了anaconda4 即已经有了python3 5 现在想切换3 7 安装3 7 命令行运行 前提是anaconda环境变量已经配置好 创建一个名为python37的环境 指定Python版本是3 7 conda会为我
  • Dictionary Learning(字典学习、稀疏表示以及其他)

    第一部分 字典学习以及稀疏表示的概要 字典学习 Dictionary Learning 和稀疏表示 Sparse Representation 在学术界的正式称谓应该是 稀疏字典学习 Sparse Dictionary Learning 该
  • js-JavaScript 对象

    1 对象定义 JavaScript 对象是拥有属性和方法的数据 2 格式 键值对 var person firstName John lastName Smith age 50 eyeColor blue move function ale
  • 后端Long型数据传给前端精度丢失问题 分布式id 解决方案

    Long型数据id传给前端精度丢失问题 数据库数据类型bigint 今天将文章类ArticleVo的数据传给前端时 发现前端接收的数据id不一样 如前端获得的id 1405916999732707300 但数据库里是id 140591699
  • springboot国际化message配置

    常常在需求中遇到国际化的要求 而国际化最常见的就是中英文切换 除了前端 后端也需要进行国际化处理 这里来记录一下我实现国际化的步骤代码 1 在resource下的i18n下建立messages properties messages en
  • CycleGAN和Conditional GAN(cGAN)

    当谈到CycleGAN和Conditional GAN cGAN 时 我们涉及到生成对抗网络 GAN 的不同变体 让我逐步介绍它们的原理和应用 CycleGAN CycleGAN是一种无监督的图像转换模型 它可以在两个不同的图像域之间进行转
  • Ubuntu Source Insight 4.0安装后首次打开报错

    系统为中文会出现 Unable to open or create 中文路径 sidb 参照网上修改regedit方法 发现没用 直接修改 wine drive c users server 我的文档 为 wine drive c user
  • C++ 中 map 容器的内存释放机制及内存碎片管理

    C 中 map 容器的内存释放机制及内存碎片管理 C 中的容器很好用 比如 vector map 等 可以动态扩容 自己管理内存 不用用户关心 但是在某些极端情况下 如果内存比较紧张的情况下 可能用户对于这些容器自己的管理规则 主要是释放规
  • Linux read命令

    读取n个字符存入变量 不用按回车 输入到第n个自动结束 student myhost read n 3 a 123 student myhost echo a 123 无回显方式读取密码 student myhost read s pass
  • windows spacemacs实现org-mode转latex,然后生成pdf

    spacemacs默认英文字体做如下修改 安装了完整版ctex套装 spacemacs增加layer gt latex 增加windows的path路径 解决org mode里中英文等宽问题 latex编译命令 pdf预览等 实现了org转
  • 什么是文件目录,文件目录项的主要内容是什么?

    文件目录是记录系统中所有文件的名字及其存放地址的目录表 表中还包括关于文件的说明信息和控制信息 主要内容如下 1 文件名 文件名分为文件的符号名和内部标识符 id号 2 文件的逻辑结构 说明该文件是否是定长 记录长度及记录个数等 3 文件的
  • USB如何布局走线

    1 先上图 USB分为2 0和3 0 2 USB布局走线需要注意的地方 静电防护 阻抗匹配 同组等长
  • cv::Mat遍历赋值的几种方式

    cv Mat赋值的几种方式 1 前言 2 Mat简介 3 遍历Mat赋值方式 方式一 方式二 方式三 4 测试 5 参考文献 1 前言 背景 获取传感器数据后需要保存成图片 有时需要对里面的元素进行操作 因为是自己开发 不能直接得到图片 所
  • CopyOnWriteArrayList部分源码分析

    CopyOnWriteArrayList部分源码分析 我们都知道ArrayList是基于数组实现的可动态扩容的集合 但是他实际上也是线程不安全的 而在JUC java util concurrent 下有个线程安全的数组集合 就是CopyO